From: Tim Starling Date: Mon, 15 Jul 2019 10:24:38 +0000 (+1000) Subject: MessageFormatterFactory X-Git-Tag: 1.34.0-rc.0~363^2~1 X-Git-Url: http://git.cyclocoop.org/%22%20.%20generer_url_ecrire%28%22suivi_revisions%22%2C%22id_auteur=%24connecte%22%29%20.%20%22?a=commitdiff_plain;h=09cd8eb0807ee7b0a94c674766e2ea201e5d071b;p=lhc%2Fweb%2Fwiklou.git MessageFormatterFactory 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 --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index b893bc9e14..abbc62c7f6 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -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/', ]; diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 6013aafc9e..c89fa4a23f 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -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 index 0000000000..101224a6a8 --- /dev/null +++ b/includes/Message/MessageFormatterFactory.php @@ -0,0 +1,29 @@ +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 index 0000000000..f5eeb16f24 --- /dev/null +++ b/includes/Message/TextFormatter.php @@ -0,0 +1,74 @@ +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 index 0000000000..10d3a4034a --- /dev/null +++ b/includes/Rest/LocalizedHttpException.php @@ -0,0 +1,11 @@ +getKey(), $code ); + } +} diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index b30726415e..7000bd3b79 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -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 index 0000000000..337ea82436 --- /dev/null +++ b/includes/libs/Message/IMessageFormatterFactory.php @@ -0,0 +1,18 @@ +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}\">$contentstype}>"; + } +} diff --git a/includes/libs/Message/ListType.php b/includes/libs/Message/ListType.php new file mode 100644 index 0000000000..60f3a82233 --- /dev/null +++ b/includes/libs/Message/ListType.php @@ -0,0 +1,22 @@ +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 index 0000000000..13b97f224a --- /dev/null +++ b/includes/libs/Message/MessageValue.php @@ -0,0 +1,258 @@ +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 '' . + $contents . ''; + } +} diff --git a/includes/libs/Message/ParamType.php b/includes/libs/Message/ParamType.php new file mode 100644 index 0000000000..890ef38ee4 --- /dev/null +++ b/includes/libs/Message/ParamType.php @@ -0,0 +1,47 @@ +type = $type; + $this->value = $value; + } + + public function dump() { + return "<{$this->type}>" . htmlspecialchars( $this->value ) . "type}>"; + } +} diff --git a/tests/phpunit/includes/Message/TextFormatterTest.php b/tests/phpunit/includes/Message/TextFormatterTest.php new file mode 100644 index 0000000000..233810fe1b --- /dev/null +++ b/tests/phpunit/includes/Message/TextFormatterTest.php @@ -0,0 +1,59 @@ +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 index 0000000000..04dfa4e7e9 --- /dev/null +++ b/tests/phpunit/includes/libs/Message/MessageValueTest.php @@ -0,0 +1,219 @@ +', + ], + [ + [ 'a' ], + 'a' + ], + [ + [ new TextParam( ParamType::BITRATE, 100 ) ], + '100' + ], + ]; + } + + /** @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( + '1x100', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testTextParamsOfType() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->textParamsOfType( ParamType::BITRATE, 1, 2 ); + $this->assertSame( '' . + '12' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testListParamsOfType() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->listParamsOfType( ListType::COMMA, [ 'a' ], [ 'b', 'c' ] ); + $this->assertSame( '' . + 'a' . + 'bc' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testTextParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->textParams( 'a', 'b' ); + $this->assertSame( '' . + 'a' . + 'b' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testNumParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->numParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testLongDurationParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->longDurationParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testShortDurationParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->shortDurationParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testExpiryParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->expiryParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testSizeParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->sizeParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testBitrateParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->bitrateParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testRawParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->rawParams( 1, 2 ); + $this->assertSame( '' . + '1' . + '2' . + '', + $mv->dump() ); + $this->assertSame( $mv, $mv2 ); + } + + public function testPlaintextParams() { + $mv = new MessageValue( 'key' ); + $mv2 = $mv->plaintextParams( 1, 2 ); + $this->assertSame( '' . + '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 ); + } +}