MessageFormatterFactory
authorTim Starling <tstarling@wikimedia.org>
Mon, 15 Jul 2019 10:24:38 +0000 (20:24 +1000)
committerTim Starling <tstarling@wikimedia.org>
Wed, 28 Aug 2019 02:28:05 +0000 (12:28 +1000)
An injectable service interface for message formatting, somewhat
narrowed compared to Message.

Only the text format is implemented in this framework so far, with
getTextFormatter() returning a formatter that converts to the text
format. Other formatters could be added to MessageFormatterFactory.

Bug: T226598
Change-Id: Id053074c1dbcb692e8309fdca602f94a385bca0c

16 files changed:
includes/AutoLoader.php
includes/MediaWikiServices.php
includes/Message/MessageFormatterFactory.php [new file with mode: 0644]
includes/Message/TextFormatter.php [new file with mode: 0644]
includes/Rest/LocalizedHttpException.php [new file with mode: 0644]
includes/ServiceWiring.php
includes/libs/Message/IMessageFormatterFactory.php [new file with mode: 0644]
includes/libs/Message/ITextFormatter.php [new file with mode: 0644]
includes/libs/Message/ListParam.php [new file with mode: 0644]
includes/libs/Message/ListType.php [new file with mode: 0644]
includes/libs/Message/MessageParam.php [new file with mode: 0644]
includes/libs/Message/MessageValue.php [new file with mode: 0644]
includes/libs/Message/ParamType.php [new file with mode: 0644]
includes/libs/Message/TextParam.php [new file with mode: 0644]
tests/phpunit/includes/Message/TextFormatterTest.php [new file with mode: 0644]
tests/phpunit/includes/libs/Message/MessageValueTest.php [new file with mode: 0644]

index b893bc9..abbc62c 100644 (file)
@@ -134,6 +134,7 @@ class AutoLoader {
                        'MediaWiki\\Edit\\' => __DIR__ . '/edit/',
                        'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
                        'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
+                       'MediaWiki\\Message\\' => __DIR__ . '/Message',
                        'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/',
                        'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
                        'MediaWiki\\Rest\\' => __DIR__ . '/Rest/',
@@ -143,6 +144,7 @@ class AutoLoader {
                        'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/',
                        'MediaWiki\\Storage\\' => __DIR__ . '/Storage/',
                        'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/',
+                       'Wikimedia\\Message\\' => __DIR__ . '/libs/Message/',
                        'Wikimedia\\ParamValidator\\' => __DIR__ . '/libs/ParamValidator/',
                        'Wikimedia\\Services\\' => __DIR__ . '/libs/services/',
                ];
index 6013aaf..c89fa4a 100644 (file)
@@ -18,6 +18,7 @@ use MediaWiki\Block\BlockManager;
 use MediaWiki\Block\BlockRestrictionStore;
 use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
 use MediaWiki\Http\HttpRequestFactory;
+use Wikimedia\Message\IMessageFormatterFactory;
 use MediaWiki\Page\MovePageFactory;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Preferences\PreferencesFactory;
@@ -709,6 +710,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'MessageCache' );
        }
 
+       /**
+        * @since 1.34
+        * @return IMessageFormatterFactory
+        */
+       public function getMessageFormatterFactory() {
+               return $this->getService( 'MessageFormatterFactory' );
+       }
+
        /**
         * @since 1.28
         * @return MimeAnalyzer
diff --git a/includes/Message/MessageFormatterFactory.php b/includes/Message/MessageFormatterFactory.php
new file mode 100644 (file)
index 0000000..101224a
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+namespace MediaWiki\Message;
+
+use Wikimedia\Message\IMessageFormatterFactory;
+use Wikimedia\Message\ITextFormatter;
+
+/**
+ * The MediaWiki-specific implementation of IMessageFormatterFactory
+ */
+class MessageFormatterFactory implements IMessageFormatterFactory {
+       private $textFormatters = [];
+
+       /**
+        * Required parameters may be added to this function without deprecation.
+        * External callers should use MediaWikiServices::getMessageFormatterFactory().
+        *
+        * @internal
+        */
+       public function __construct() {
+       }
+
+       public function getTextFormatter( $langCode ): ITextFormatter {
+               if ( !isset( $this->textFormatters[$langCode] ) ) {
+                       $this->textFormatters[$langCode] = new TextFormatter( $langCode );
+               }
+               return $this->textFormatters[$langCode];
+       }
+}
diff --git a/includes/Message/TextFormatter.php b/includes/Message/TextFormatter.php
new file mode 100644 (file)
index 0000000..f5eeb16
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+namespace MediaWiki\Message;
+
+use Wikimedia\Message\ITextFormatter;
+use Wikimedia\Message\ListParam;
+use Wikimedia\Message\MessageParam;
+use Wikimedia\Message\MessageValue;
+use Wikimedia\Message\ParamType;
+use Message;
+
+/**
+ * The MediaWiki-specific implementation of ITextFormatter
+ */
+class TextFormatter implements ITextFormatter {
+       /** @var string */
+       private $langCode;
+
+       /**
+        * Construct a TextFormatter.
+        *
+        * The type signature may change without notice as dependencies are added
+        * to the constructor. External callers should use
+        * MediaWikiServices::getMessageFormatterFactory()
+        *
+        * @internal
+        */
+       public function __construct( $langCode ) {
+               $this->langCode = $langCode;
+       }
+
+       /**
+        * Allow the Message class to be mocked in tests by constructing objects in
+        * a protected method.
+        *
+        * @internal
+        * @param string $key
+        * @return Message
+        */
+       protected function createMessage( $key ) {
+               return new Message( $key );
+       }
+
+       public function getLangCode() {
+               return $this->langCode;
+       }
+
+       private static function convertParam( MessageParam $param ) {
+               if ( $param instanceof ListParam ) {
+                       $convertedElements = [];
+                       foreach ( $param->getValue() as $element ) {
+                               $convertedElements[] = self::convertParam( $element );
+                       }
+                       return Message::listParam( $convertedElements, $param->getListType() );
+               } elseif ( $param instanceof MessageParam ) {
+                       if ( $param->getType() === ParamType::TEXT ) {
+                               return $param->getValue();
+                       } else {
+                               return [ $param->getType() => $param->getValue() ];
+                       }
+               } else {
+                       throw new \InvalidArgumentException( 'Invalid message parameter type' );
+               }
+       }
+
+       public function format( MessageValue $mv ) {
+               $message = $this->createMessage( $mv->getKey() );
+               foreach ( $mv->getParams() as $param ) {
+                       $message->params( self::convertParam( $param ) );
+               }
+               $message->inLanguage( $this->langCode );
+               return $message->text();
+       }
+}
diff --git a/includes/Rest/LocalizedHttpException.php b/includes/Rest/LocalizedHttpException.php
new file mode 100644 (file)
index 0000000..10d3a40
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+
+namespace MediaWiki\Rest;
+
+use Wikimedia\Message\MessageValue;
+
+class LocalizedHttpException extends HttpException {
+       public function __construct( MessageValue $message, $code = 500 ) {
+               parent::__construct( 'Localized exception with key ' . $message->getKey(), $code );
+       }
+}
index b307264..7000bd3 100644 (file)
@@ -52,6 +52,8 @@ use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use Wikimedia\Message\IMessageFormatterFactory;
+use MediaWiki\Message\MessageFormatterFactory;
 use MediaWiki\Page\MovePageFactory;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Preferences\PreferencesFactory;
@@ -350,6 +352,11 @@ return [
                );
        },
 
+       'MessageFormatterFactory' =>
+       function ( MediaWikiServices $services ) : IMessageFormatterFactory {
+               return new MessageFormatterFactory();
+       },
+
        'MimeAnalyzer' => function ( MediaWikiServices $services ) : MimeAnalyzer {
                $logger = LoggerFactory::getInstance( 'Mime' );
                $mainConfig = $services->getMainConfig();
diff --git a/includes/libs/Message/IMessageFormatterFactory.php b/includes/libs/Message/IMessageFormatterFactory.php
new file mode 100644 (file)
index 0000000..337ea82
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * A simple factory providing a message formatter for a given language code.
+ *
+ * @see ITextFormatter
+ */
+interface IMessageFormatterFactory {
+       /**
+        * Get a text message formatter for a given language.
+        *
+        * @param string $langCode The language code
+        * @return ITextFormatter
+        */
+       public function getTextFormatter( $langCode ): ITextFormatter;
+}
diff --git a/includes/libs/Message/ITextFormatter.php b/includes/libs/Message/ITextFormatter.php
new file mode 100644 (file)
index 0000000..00f6e99
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * ITextFormatter is a simplified interface to the Message class. It converts
+ * MessageValue message specifiers to localized text in a certain language.
+ *
+ * MessageValue supports message keys, and parameters with a wide variety of
+ * types. It does not expose any details of how messages are retrieved from
+ * storage or what format they are stored in.
+ *
+ * Thus, TextFormatter supports single message keys, but not the concept of
+ * presence or absence of a key from storage. So it does not support
+ * fallback sequences of multiple keys.
+ *
+ * The caller cannot modify the details of message translation, such as which
+ * of multiple sources the message is taken from. Any such flags may be injected
+ * into the factory constructor.
+ *
+ * Implementations of TextFormatter are not required to perfectly format
+ * any message in any language. Implementations should make a best effort to
+ * produce human-readable text.
+ *
+ * @package MediaWiki\MessageFormatter
+ */
+interface ITextFormatter {
+       /**
+        * Get the internal language code in which format() is
+        * @return string
+        */
+       function getLangCode();
+
+       /**
+        * Convert a MessageValue to text.
+        *
+        * The result is not safe for use as raw HTML.
+        *
+        * @param MessageValue $message
+        * @return string
+        */
+       function format( MessageValue $message );
+}
diff --git a/includes/libs/Message/ListParam.php b/includes/libs/Message/ListParam.php
new file mode 100644 (file)
index 0000000..c6a9c65
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * The class for list parameters
+ */
+class ListParam extends MessageParam {
+       private $listType;
+
+       /**
+        * @param string $listType One of the ListType constants:
+        *   - ListType::COMMA: A comma-separated list
+        *   - ListType::SEMICOLON: A semicolon-separated list
+        *   - ListType::PIPE: A pipe-separated list
+        *   - ListType::TEXT: A natural language list, separated by commas and
+        *     the word "and".
+        * @param (MessageParam|string)[] $elements An array of parameters
+        */
+       public function __construct( $listType, array $elements ) {
+               $this->type = ParamType::LIST;
+               $this->listType = $listType;
+               $this->value = [];
+               foreach ( $elements as $element ) {
+                       if ( $element instanceof MessageParam ) {
+                               $this->value[] = $element;
+                       } elseif ( is_scalar( $element ) ) {
+                               $this->value[] = new TextParam( ParamType::TEXT, $element );
+                       } else {
+                               throw new \InvalidArgumentException(
+                                       'ListParam elements must be MessageParam or scalar' );
+                       }
+               }
+       }
+
+       /**
+        * Get the type of the list
+        *
+        * @return string One of the ListType constants
+        */
+       public function getListType() {
+               return $this->listType;
+       }
+
+       public function dump() {
+               $contents = '';
+               foreach ( $this->value as $element ) {
+                       $contents .= $element->dump();
+               }
+               return "<{$this->type} listType=\"{$this->listType}\">$contents</{$this->type}>";
+       }
+}
diff --git a/includes/libs/Message/ListType.php b/includes/libs/Message/ListType.php
new file mode 100644 (file)
index 0000000..60f3a82
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * The constants used to specify list types. The values of the constants are an
+ * unstable implementation detail and correspond to the names of the list types
+ * in the Message class.
+ */
+class ListType {
+       /** A comma-separated list */
+       const COMMA = 'comma';
+
+       /** A semicolon-separated list */
+       const SEMICOLON = 'semicolon';
+
+       /** A pipe-separated list */
+       const PIPE = 'pipe';
+
+       /** A natural-language list separated by "and" */
+       const AND = 'text';
+}
diff --git a/includes/libs/Message/MessageParam.php b/includes/libs/Message/MessageParam.php
new file mode 100644 (file)
index 0000000..8162212
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * The base class for message parameters.
+ */
+abstract class MessageParam {
+       protected $type;
+       protected $value;
+
+       /**
+        * Get the type of the parameter.
+        *
+        * @return string One of the ParamType constants
+        */
+       public function getType() {
+               return $this->type;
+       }
+
+       /**
+        * Get the input value of the parameter
+        *
+        * @return int|float|string|array
+        */
+       public function getValue() {
+               return $this->value;
+       }
+
+       /**
+        * Dump the object for testing/debugging
+        *
+        * @return string
+        */
+       abstract public function dump();
+}
diff --git a/includes/libs/Message/MessageValue.php b/includes/libs/Message/MessageValue.php
new file mode 100644 (file)
index 0000000..13b97f2
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * A MessageValue holds a key and an array of parameters
+ */
+class MessageValue {
+       /** @var string */
+       private $key;
+
+       /** @var MessageParam[] */
+       private $params;
+
+       /**
+        * @param string $key
+        * @param array $params Each element of the parameter array
+        *   may be either a MessageParam or a scalar. If it is a scalar, it is
+        *   converted to a parameter of type TEXT.
+        */
+       public function __construct( $key, $params = [] ) {
+               $this->key = $key;
+               $this->params = [];
+               $this->params( ...$params );
+       }
+
+       /**
+        * Get the message key
+        *
+        * @return string
+        */
+       public function getKey() {
+               return $this->key;
+       }
+
+       /**
+        * Get the parameter array
+        *
+        * @return MessageParam[]
+        */
+       public function getParams() {
+               return $this->params;
+       }
+
+       /**
+        * Chainable mutator which adds text parameters and MessageParam parameters
+        *
+        * @param mixed ...$values Scalar or MessageParam values
+        * @return MessageValue
+        */
+       public function params( ...$values ) {
+               foreach ( $values as $value ) {
+                       if ( $value instanceof MessageParam ) {
+                               $this->params[] = $value;
+                       } else {
+                               $this->params[] = new TextParam( ParamType::TEXT, $value );
+                       }
+               }
+               return $this;
+       }
+
+       /**
+        * Chainable mutator which adds text parameters with a common type
+        *
+        * @param string $type One of the ParamType constants
+        * @param mixed ...$values Scalar values
+        * @return MessageValue
+        */
+       public function textParamsOfType( $type, ...$values ) {
+               foreach ( $values as $value ) {
+                       $this->params[] = new TextParam( $type, $value );
+               }
+               return $this;
+       }
+
+       /**
+        * Chainable mutator which adds list parameters with a common type
+        *
+        * @param string $listType One of the ListType constants
+        * @param array ...$values Each value should be an array of list items.
+        * @return MessageValue
+        */
+       public function listParamsOfType( $listType, ...$values ) {
+               foreach ( $values as $value ) {
+                       $this->params[] = new ListParam( $listType, $value );
+               }
+               return $this;
+       }
+
+       /**
+        * Chainable mutator which adds parameters of type text.
+        *
+        * @param string ...$values
+        * @return MessageValue
+        */
+       public function textParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::TEXT, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds numeric parameters
+        *
+        * @param mixed ...$values
+        * @return MessageValue
+        */
+       public function numParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::NUM, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters which are a duration specified
+        * in seconds. This is similar to timePeriodParams() except that the result
+        * will be more verbose.
+        *
+        * @param int|float ...$values
+        * @return MessageValue
+        */
+       public function longDurationParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::DURATION_LONG, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters which are a time period in seconds.
+        * This is similar to durationParams() except that the result will be more
+        * compact.
+        *
+        * @param int|float ...$values
+        * @return MessageValue
+        */
+       public function shortDurationParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::DURATION_SHORT, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters which are an expiry timestamp
+        * as used in the MediaWiki database schema.
+        *
+        * @param string ...$values
+        * @return MessageValue
+        */
+       public function expiryParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::EXPIRY, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters which are a number of bytes.
+        *
+        * @param int ...$values
+        * @return MessageValue
+        */
+       public function sizeParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::SIZE, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters which are a number of bits per
+        * second.
+        *
+        * @param int|float ...$values
+        * @return MessageValue
+        */
+       public function bitrateParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::BITRATE, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters of type "raw".
+        *
+        * @param mixed ...$values
+        * @return MessageValue
+        */
+       public function rawParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::RAW, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds parameters of type "plaintext".
+        */
+       public function plaintextParams( ...$values ) {
+               return $this->textParamsOfType( ParamType::PLAINTEXT, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds comma lists. Each comma list is an array of
+        * list elements, and each list element is either a MessageParam or a
+        * string. String parameters are converted to parameters of type "text".
+        *
+        * The list parameters thus created are formatted as a comma-separated list,
+        * or some local equivalent.
+        *
+        * @param (MessageParam|string)[] ...$values
+        * @return MessageValue
+        */
+       public function commaListParams( ...$values ) {
+               return $this->listParamsOfType( ListType::COMMA, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds semicolon lists. Each semicolon list is an
+        * array of list elements, and each list element is either a MessageParam
+        * or a string. String parameters are converted to parameters of type
+        * "text".
+        *
+        * The list parameters thus created are formatted as a semicolon-separated
+        * list, or some local equivalent.
+        *
+        * @param (MessageParam|string)[] ...$values
+        * @return MessageValue
+        */
+       public function semicolonListParams( ...$values ) {
+               return $this->listParamsOfType( ListType::SEMICOLON, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds pipe lists. Each pipe list is an array of
+        * list elements, and each list element is either a MessageParam or a
+        * string. String parameters are converted to parameters of type "text".
+        *
+        * The list parameters thus created are formatted as a pipe ("|") -separated
+        * list, or some local equivalent.
+        *
+        * @param (MessageParam|string)[] ...$values
+        * @return MessageValue
+        */
+       public function pipeListParams( ...$values ) {
+               return $this->listParamsOfType( ListType::PIPE, ...$values );
+       }
+
+       /**
+        * Chainable mutator which adds text lists. Each text list is an array of
+        * list elements, and each list element is either a MessageParam or a
+        * string. String parameters are converted to parameters of type "text".
+        *
+        * The list parameters thus created, when formatted, are joined as in natural
+        * language. In English, this means a comma-separated list, with the last
+        * two elements joined with "and".
+        *
+        * @param (MessageParam|string)[] ...$values
+        * @return MessageValue
+        */
+       public function textListParams( ...$values ) {
+               return $this->listParamsOfType( ListType::AND, ...$values );
+       }
+
+       /**
+        * Dump the object for testing/debugging
+        *
+        * @return string
+        */
+       public function dump() {
+               $contents = '';
+               foreach ( $this->params as $param ) {
+                       $contents .= $param->dump();
+               }
+               return '<message key="' . htmlspecialchars( $this->key ) . '">' .
+                       $contents . '</message>';
+       }
+}
diff --git a/includes/libs/Message/ParamType.php b/includes/libs/Message/ParamType.php
new file mode 100644 (file)
index 0000000..890ef38
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * The constants used to specify parameter types. The values of the constants
+ * are an unstable implementation detail, and correspond to the names of the
+ * parameter types in the Message class.
+ */
+class ParamType {
+       /** A simple text parameter */
+       const TEXT = 'text';
+
+       /** A number, to be formatted using local digits and separators */
+       const NUM = 'num';
+
+       /** A number of seconds, to be formatted as natural language text. */
+       const DURATION_LONG = 'duration';
+
+       /** A number of seconds, to be formatted in an abbreviated way. */
+       const DURATION_SHORT = 'timeperiod';
+
+       /**
+        * An expiry time for a block. The input is either a timestamp in one
+        * of the formats accepted by the Wikimedia\Timestamp library, or
+        * "infinity" for an infinite block.
+        */
+       const EXPIRY = 'expiry';
+
+       /** A number of bytes. */
+       const SIZE = 'size';
+
+       /** A number of bits per second. */
+       const BITRATE = 'bitrate';
+
+       /** The list type (ListParam) */
+       const LIST = 'list';
+
+       /**
+        * A text parameter which is substituted after preprocessing, and so is
+        * not available to the preprocessor and cannot be modified by it.
+        */
+       const RAW = 'raw';
+
+       /** Reserved for future use. */
+       const PLAINTEXT = 'plaintext';
+}
diff --git a/includes/libs/Message/TextParam.php b/includes/libs/Message/TextParam.php
new file mode 100644 (file)
index 0000000..c1a1f08
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+namespace Wikimedia\Message;
+
+class TextParam extends MessageParam {
+       /**
+        * Construct a text parameter
+        *
+        * @param string $type May be one of:
+        *   - ParamType::TEXT: A simple text parameter
+        *   - ParamType::NUM: A number, to be formatted using local digits and
+        *     separators
+        *   - ParamType::DURATION_LONG: A number of seconds, to be formatted as natural
+        *     language text.
+        *   - ParamType::DURATION_SHORT: A number of seconds, to be formatted in an
+        *     abbreviated way.
+        *   - ParamType::EXPIRY: An expiry time for a block. The input is either
+        *     a timestamp in one of the formats accepted by the Wikimedia\Timestamp
+        *     library, or "infinity" for an infinite block.
+        *   - ParamType::SIZE: A number of bytes.
+        *   - ParamType::BITRATE: A number of bits per second.
+        *   - ParamType::RAW: A text parameter which is substituted after
+        *     preprocessing, and so is not available to the preprocessor and cannot
+        *     be modified by it.
+        *   - ParamType::PLAINTEXT: Reserved for future use.
+        *
+        * @param string|int|float $value
+        */
+       public function __construct( $type, $value ) {
+               $this->type = $type;
+               $this->value = $value;
+       }
+
+       public function dump() {
+               return "<{$this->type}>" . htmlspecialchars( $this->value ) . "</{$this->type}>";
+       }
+}
diff --git a/tests/phpunit/includes/Message/TextFormatterTest.php b/tests/phpunit/includes/Message/TextFormatterTest.php
new file mode 100644 (file)
index 0000000..233810f
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace MediaWiki\Tests\Message;
+
+use MediaWiki\Message\TextFormatter;
+use MediaWikiTestCase;
+use Message;
+use Wikimedia\Message\MessageValue;
+use Wikimedia\Message\ParamType;
+use Wikimedia\Message\TextParam;
+
+/**
+ * @covers \MediaWiki\Message\TextFormatter
+ * @covers \Wikimedia\Message\MessageValue
+ * @covers \Wikimedia\Message\ListParam
+ * @covers \Wikimedia\Message\TextParam
+ * @covers \Wikimedia\Message\MessageParam
+ */
+class TextFormatterTest extends MediaWikiTestCase {
+       private function createTextFormatter( $langCode ) {
+               return new class( $langCode ) extends TextFormatter {
+                       public function __construct( $langCode ) {
+                               parent::__construct( $langCode );
+                       }
+
+                       protected function createMessage( $key ) {
+                               return new FakeMessage( $key );
+                       }
+               };
+       }
+
+       public function testGetLangCode() {
+               $formatter = $this->createTextFormatter( 'fr' );
+               $this->assertSame( 'fr', $formatter->getLangCode() );
+       }
+
+       public function testFormatBitrate() {
+               $formatter = $this->createTextFormatter( 'en' );
+               $mv = ( new MessageValue( 'test' ) )->bitrateParams( 100, 200 );
+               $result = $formatter->format( $mv );
+               $this->assertSame( 'test 100 bps 200 bps', $result );
+       }
+
+       public function testFormatList() {
+               $formatter = $this->createTextFormatter( 'en' );
+               $mv = ( new MessageValue( 'test' ) )->commaListParams( [
+                       'a',
+                       new TextParam( ParamType::BITRATE, 100 ),
+               ] );
+               $result = $formatter->format( $mv );
+               $this->assertSame( 'test a, 100 bps $2', $result );
+       }
+}
+
+class FakeMessage extends Message {
+       public function fetchMessage() {
+               return "{$this->getKey()} $1 $2";
+       }
+}
diff --git a/tests/phpunit/includes/libs/Message/MessageValueTest.php b/tests/phpunit/includes/libs/Message/MessageValueTest.php
new file mode 100644 (file)
index 0000000..04dfa4e
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+
+namespace Wikimedia\Tests\Message;
+
+use Wikimedia\Message\ListType;
+use Wikimedia\Message\MessageValue;
+use Wikimedia\Message\ParamType;
+use Wikimedia\Message\TextParam;
+use MediaWikiTestCase;
+
+/**
+ * @covers \Wikimedia\Message\MessageValue
+ * @covers \Wikimedia\Message\ListParam
+ * @covers \Wikimedia\Message\TextParam
+ * @covers \Wikimedia\Message\MessageParam
+ */
+class MessageValueTest extends MediaWikiTestCase {
+       public static function provideConstruct() {
+               return [
+                       [
+                               [],
+                               '<message key="key"></message>',
+                       ],
+                       [
+                               [ 'a' ],
+                               '<message key="key"><text>a</text></message>'
+                       ],
+                       [
+                               [ new TextParam( ParamType::BITRATE, 100 ) ],
+                               '<message key="key"><bitrate>100</bitrate></message>'
+                       ],
+               ];
+       }
+
+       /** @dataProvider provideConstruct */
+       public function testConstruct( $input, $expected ) {
+               $mv = new MessageValue( 'key', $input );
+               $this->assertSame( $expected, $mv->dump() );
+       }
+
+       public function testGetKey() {
+               $mv = new MessageValue( 'key' );
+               $this->assertSame( 'key', $mv->getKey() );
+       }
+
+       public function testParams() {
+               $mv = new MessageValue( 'key' );
+               $mv->params( 1, 'x' );
+               $mv2 = $mv->params( new TextParam( ParamType::BITRATE, 100 ) );
+               $this->assertSame(
+                       '<message key="key"><text>1</text><text>x</text><bitrate>100</bitrate></message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testTextParamsOfType() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->textParamsOfType( ParamType::BITRATE, 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<bitrate>1</bitrate><bitrate>2</bitrate>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testListParamsOfType() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->listParamsOfType( ListType::COMMA, [ 'a' ], [ 'b', 'c' ] );
+               $this->assertSame( '<message key="key">' .
+                       '<list listType="comma"><text>a</text></list>' .
+                       '<list listType="comma"><text>b</text><text>c</text></list>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testTextParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->textParams( 'a', 'b' );
+               $this->assertSame( '<message key="key">' .
+                       '<text>a</text>' .
+                       '<text>b</text>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testNumParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->numParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<num>1</num>' .
+                       '<num>2</num>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testLongDurationParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->longDurationParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<duration>1</duration>' .
+                       '<duration>2</duration>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testShortDurationParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->shortDurationParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<timeperiod>1</timeperiod>' .
+                       '<timeperiod>2</timeperiod>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testExpiryParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->expiryParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<expiry>1</expiry>' .
+                       '<expiry>2</expiry>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testSizeParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->sizeParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<size>1</size>' .
+                       '<size>2</size>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testBitrateParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->bitrateParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<bitrate>1</bitrate>' .
+                       '<bitrate>2</bitrate>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testRawParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->rawParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<raw>1</raw>' .
+                       '<raw>2</raw>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testPlaintextParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->plaintextParams( 1, 2 );
+               $this->assertSame( '<message key="key">' .
+                       '<plaintext>1</plaintext>' .
+                       '<plaintext>2</plaintext>' .
+                       '</message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testCommaListParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->commaListParams( [ 'a', 'b' ] );
+               $this->assertSame( '<message key="key">' .
+                       '<list listType="comma">' .
+                       '<text>a</text><text>b</text>' .
+                       '</list></message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function tesSemicolonListParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->semicolonListParams( [ 'a', 'b' ] );
+               $this->assertSame( '<message key="key">' .
+                       '<list listType="semicolon">' .
+                       '<text>a</text><text>b</text>' .
+                       '</list></message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testPipeListParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->pipeListParams( [ 'a', 'b' ] );
+               $this->assertSame( '<message key="key">' .
+                       '<list listType="pipe">' .
+                       '<text>a</text><text>b</text>' .
+                       '</list></message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+
+       public function testTextListParams() {
+               $mv = new MessageValue( 'key' );
+               $mv2 = $mv->textListParams( [ 'a', 'b' ] );
+               $this->assertSame( '<message key="key">' .
+                       '<list listType="text">' .
+                       '<text>a</text><text>b</text>' .
+                       '</list></message>',
+                       $mv->dump() );
+               $this->assertSame( $mv, $mv2 );
+       }
+}