libs/Message: Improve documentation
authorBrad Jorsch <bjorsch@wikimedia.org>
Thu, 29 Aug 2019 20:52:27 +0000 (16:52 -0400)
committerBrad Jorsch <bjorsch@wikimedia.org>
Thu, 29 Aug 2019 21:04:01 +0000 (17:04 -0400)
Among other things, this removed mention of MediaWiki classes from the
library and adds a README.md that attempts to define constraints for
interoperability between libraries producing MessageValues and formatter
implementations that are expected to handle them.

This also renames "TextParam" to "ScalarParam", as that seems a more
accurate name for the class.

Change-Id: I264dd4de394d734a87929cf4740779e7b7d0e04a

includes/libs/Message/ITextFormatter.php
includes/libs/Message/ListParam.php
includes/libs/Message/ListType.php
includes/libs/Message/MessageParam.php
includes/libs/Message/MessageValue.php
includes/libs/Message/ParamType.php
includes/libs/Message/README.md [new file with mode: 0644]
includes/libs/Message/ScalarParam.php [new file with mode: 0644]
includes/libs/Message/TextParam.php [deleted file]
tests/phpunit/includes/Message/TextFormatterTest.php
tests/phpunit/includes/libs/Message/MessageValueTest.php

index 00f6e99..c433e47 100644 (file)
@@ -3,16 +3,7 @@
 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.
+ * Converts MessageValue message specifiers to localized plain text in a certain language.
  *
  * 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
index c6a9c65..d550fec 100644 (file)
@@ -3,19 +3,17 @@
 namespace Wikimedia\Message;
 
 /**
- * The class for list parameters
+ * Value object representing a message parameter that consists of a list of values.
+ *
+ * Message parameter classes are pure value objects and are safely newable.
  */
 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
+        * @param string $listType One of the ListType constants.
+        * @param (MessageParam|MessageValue|string|int|float)[] $elements Values in the list.
+        *  Values that are not instances of MessageParam are wrapped using ParamType::TEXT.
         */
        public function __construct( $listType, array $elements ) {
                $this->type = ParamType::LIST;
@@ -25,7 +23,7 @@ class ListParam extends MessageParam {
                        if ( $element instanceof MessageParam ) {
                                $this->value[] = $element;
                        } elseif ( is_scalar( $element ) ) {
-                               $this->value[] = new TextParam( ParamType::TEXT, $element );
+                               $this->value[] = new ScalarParam( ParamType::TEXT, $element );
                        } else {
                                throw new \InvalidArgumentException(
                                        'ListParam elements must be MessageParam or scalar' );
index 60f3a82..f846464 100644 (file)
@@ -4,8 +4,7 @@ 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.
+ * unstable implementation detail.
  */
 class ListType {
        /** A comma-separated list */
index 8162212..b6475a7 100644 (file)
@@ -3,7 +3,9 @@
 namespace Wikimedia\Message;
 
 /**
- * The base class for message parameters.
+ * Value object representing a message parameter that consists of a list of values.
+ *
+ * Message parameter classes are pure value objects and are safely newable.
  */
 abstract class MessageParam {
        protected $type;
@@ -21,7 +23,7 @@ abstract class MessageParam {
        /**
         * Get the input value of the parameter
         *
-        * @return int|float|string|array
+        * @return mixed
         */
        public function getValue() {
                return $this->value;
index 13b97f2..1d80d60 100644 (file)
@@ -3,7 +3,13 @@
 namespace Wikimedia\Message;
 
 /**
- * A MessageValue holds a key and an array of parameters
+ * Value object representing a message for i18n.
+ *
+ * A MessageValue holds a key and an array of parameters. It can be converted
+ * to a string in a particular language using formatters obtained from an
+ * IMessageFormatterFactory.
+ *
+ * MessageValues are pure value objects and are safely newable.
  */
 class MessageValue {
        /** @var string */
@@ -14,9 +20,8 @@ class MessageValue {
 
        /**
         * @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.
+        * @param (MessageParam|MessageValue|string|int|float)[] $params Values that are not instances
+        *  of MessageParam are wrapped using ParamType::TEXT.
         */
        public function __construct( $key, $params = [] ) {
                $this->key = $key;
@@ -45,7 +50,7 @@ class MessageValue {
        /**
         * Chainable mutator which adds text parameters and MessageParam parameters
         *
-        * @param mixed ...$values Scalar or MessageParam values
+        * @param MessageParam|MessageValue|string|int|float ...$values
         * @return MessageValue
         */
        public function params( ...$values ) {
@@ -53,7 +58,7 @@ class MessageValue {
                        if ( $value instanceof MessageParam ) {
                                $this->params[] = $value;
                        } else {
-                               $this->params[] = new TextParam( ParamType::TEXT, $value );
+                               $this->params[] = new ScalarParam( ParamType::TEXT, $value );
                        }
                }
                return $this;
@@ -63,12 +68,12 @@ class MessageValue {
         * Chainable mutator which adds text parameters with a common type
         *
         * @param string $type One of the ParamType constants
-        * @param mixed ...$values Scalar values
+        * @param MessageValue|string|int|float ...$values Scalar values
         * @return MessageValue
         */
        public function textParamsOfType( $type, ...$values ) {
                foreach ( $values as $value ) {
-                       $this->params[] = new TextParam( $type, $value );
+                       $this->params[] = new ScalarParam( $type, $value );
                }
                return $this;
        }
@@ -77,7 +82,8 @@ class MessageValue {
         * 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.
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function listParamsOfType( $listType, ...$values ) {
@@ -88,9 +94,9 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters of type text.
+        * Chainable mutator which adds parameters of type text (ParamType::TEXT).
         *
-        * @param string ...$values
+        * @param MessageValue|string|int|float ...$values
         * @return MessageValue
         */
        public function textParams( ...$values ) {
@@ -98,9 +104,9 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds numeric parameters
+        * Chainable mutator which adds numeric parameters (ParamType::NUM).
         *
-        * @param mixed ...$values
+        * @param int|float ...$values
         * @return MessageValue
         */
        public function numParams( ...$values ) {
@@ -109,8 +115,10 @@ class MessageValue {
 
        /**
         * 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.
+        * in seconds (ParamType::DURATION_LONG).
+        *
+        * This is similar to shorDurationParams() except that the result will be
+        * more verbose.
         *
         * @param int|float ...$values
         * @return MessageValue
@@ -120,8 +128,10 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters which are a time period in seconds.
-        * This is similar to durationParams() except that the result will be more
+        * Chainable mutator which adds parameters which are a duration specified
+        * in seconds (ParamType::DURATION_SHORT).
+        *
+        * This is similar to longDurationParams() except that the result will be more
         * compact.
         *
         * @param int|float ...$values
@@ -132,10 +142,10 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters which are an expiry timestamp
-        * as used in the MediaWiki database schema.
+        * Chainable mutator which adds parameters which are an expiry timestamp (ParamType::EXPIRY).
         *
-        * @param string ...$values
+        * @param string ...$values Timestamp as accepted by the Wikimedia\Timestamp library,
+        *  or "infinity"
         * @return MessageValue
         */
        public function expiryParams( ...$values ) {
@@ -143,7 +153,7 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters which are a number of bytes.
+        * Chainable mutator which adds parameters which are a number of bytes (ParamType::SIZE).
         *
         * @param int ...$values
         * @return MessageValue
@@ -154,7 +164,7 @@ class MessageValue {
 
        /**
         * Chainable mutator which adds parameters which are a number of bits per
-        * second.
+        * second (ParamType::BITRATE).
         *
         * @param int|float ...$values
         * @return MessageValue
@@ -164,9 +174,13 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters of type "raw".
+        * Chainable mutator which adds "raw" parameters (ParamType::RAW).
         *
-        * @param mixed ...$values
+        * Raw parameters are substituted after formatter processing. The caller is responsible
+        * for ensuring that the value will be safe for the intended output format, and
+        * documenting what that intended output format is.
+        *
+        * @param string ...$values
         * @return MessageValue
         */
        public function rawParams( ...$values ) {
@@ -174,21 +188,27 @@ class MessageValue {
        }
 
        /**
-        * Chainable mutator which adds parameters of type "plaintext".
+        * Chainable mutator which adds plaintext parameters (ParamType::PLAINTEXT).
+        *
+        * Plaintext parameters are substituted after formatter processing. The value
+        * will be escaped by the formatter as appropriate for the target output format
+        * so as to be represented as plain text rather than as any sort of markup.
+        *
+        * @param string ...$values
+        * @return MessageValue
         */
        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".
+        * Chainable mutator which adds comma lists (ListType::COMMA).
         *
         * The list parameters thus created are formatted as a comma-separated list,
         * or some local equivalent.
         *
-        * @param (MessageParam|string)[] ...$values
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function commaListParams( ...$values ) {
@@ -196,15 +216,13 @@ class MessageValue {
        }
 
        /**
-        * 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".
+        * Chainable mutator which adds semicolon lists (ListType::SEMICOLON).
         *
         * The list parameters thus created are formatted as a semicolon-separated
         * list, or some local equivalent.
         *
-        * @param (MessageParam|string)[] ...$values
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function semicolonListParams( ...$values ) {
@@ -212,14 +230,13 @@ class MessageValue {
        }
 
        /**
-        * 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".
+        * Chainable mutator which adds pipe lists (ListType::PIPE).
         *
         * The list parameters thus created are formatted as a pipe ("|") -separated
         * list, or some local equivalent.
         *
-        * @param (MessageParam|string)[] ...$values
+        * @param (MessageParam|MessageValue|string|int|float)[] ...$values Each value
+        *  is an array of items suitable to pass as $params to ListParam::__construct()
         * @return MessageValue
         */
        public function pipeListParams( ...$values ) {
@@ -227,9 +244,7 @@ class MessageValue {
        }
 
        /**
-        * 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".
+        * Chainable mutator which adds natural-language lists (ListType::AND).
         *
         * The list parameters thus created, when formatted, are joined as in natural
         * language. In English, this means a comma-separated list, with the last
index 890ef38..4db7112 100644 (file)
@@ -4,44 +4,62 @@ 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.
+ * are an unstable implementation detail.
+ *
+ * Unless otherwise noted, these should be used with an instance of ScalarParam.
  */
 class ParamType {
-       /** A simple text parameter */
+       /** A simple text string or another MessageValue, not otherwise formatted. */
        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. */
+       /**
+        * A number of seconds, to be formatted as natural language text.
+        * The value will be output exactly.
+        */
        const DURATION_LONG = 'duration';
 
-       /** A number of seconds, to be formatted in an abbreviated way. */
+       /**
+        * A number of seconds, to be formatted as natural language text in an abbreviated way.
+        * The output will be rounded to an appropriate magnitude.
+        */
        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.
+        * An expiry time.
+        *
+        * The input is either a timestamp in one of the formats accepted by the
+        * Wikimedia\Timestamp library, or "infinity" if the thing doesn't expire.
+        *
+        * The output is a date and time in local format, or a string representing
+        * an "infinite" expiry.
         */
        const EXPIRY = 'expiry';
 
-       /** A number of bytes. */
+       /** A number of bytes. The output will be rounded to an appropriate magnitude. */
        const SIZE = 'size';
 
-       /** A number of bits per second. */
+       /** A number of bits per second. The output will be rounded to an appropriate magnitude. */
        const BITRATE = 'bitrate';
 
-       /** The list type (ListParam) */
+       /** A list of values. Must be used with 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.
+        * A text parameter which is substituted after formatter processing.
+        *
+        * The creator of the parameter and message is responsible for ensuring
+        * that the value will be safe for the intended output format, and
+        * documenting what that intended output format is.
         */
        const RAW = 'raw';
 
-       /** Reserved for future use. */
+       /**
+        * A text parameter which is substituted after formatter processing.
+        * The output will be escaped as appropriate for the output format so
+        * as to represent plain text rather than any sort of markup.
+        */
        const PLAINTEXT = 'plaintext';
 }
diff --git a/includes/libs/Message/README.md b/includes/libs/Message/README.md
new file mode 100644 (file)
index 0000000..9f6255a
--- /dev/null
@@ -0,0 +1,291 @@
+Wikimedia Internationalization Library
+======================================
+
+This library provides interfaces and value objects for internationalization (i18n)
+of applications in PHP.
+
+It is based on the i18n code used in MediaWiki, and is also intended to be
+compatible with [jQuery.i18n], a JavaScript i18n library.
+
+Concepts
+--------
+
+Any text string that is needed in an application is a **message**. This might
+be something like a button label, a sentence, or a longer text. Each message is
+assigned a **message key**, which is used as the identifier in code.
+
+Each message is translated into various languages, each represented by a
+**language code**. The message's text (as translated into each language) can
+contain **placeholders**, which represents a place in the message where a
+**parameter** is to be inserted, and **formatting commands**. It might be plain
+text other than these placeholders and formatting commands, or it might be in a
+**markup language** such as wikitext or Markdown.
+
+A **formatter** is used to convert the message key and parameters into a text
+representation in a particular language and **output format**.
+
+The library itself imposes few restrictions on all of these concepts; this
+document contains recommendations to help various implementations operate in
+compatible ways.
+
+Usage
+-----
+
+<pre lang="php">
+use Wikimedia\Message\MessageValue;
+use Wikimedia\Message\MessageParam;
+use Wikimedia\Message\ParamType;
+
+// Constructor interface
+$message = new MessageValue( 'message-key', [
+    'parameter',
+    new MessageValue( 'another-message' ),
+    new MessageParam( ParamType::NUM, 12345 ),
+] );
+
+// Fluent interface
+$message = ( new MessageValue( 'message-key' ) )
+    ->params( 'parameter', new MessageValue( 'another-message' ) )
+    ->numParams( 12345 );
+
+// Formatting
+$messageFormatter = $serviceContainter->get( 'MessageFormatterFactory' )->getTextFormatter( 'de' );
+$output = $messageFormatter->format( $message );
+</pre>
+
+Class Overview
+--------------
+
+### Messages
+
+Messages and their parameters are represented by newable value objects.
+
+**MessageValue** represents an instance of a message, holding the key and any
+parameters. It is mutable in that parameters can be added to the object after
+creation.
+
+**MessageParam** is an abstract value class representing a parameter to a message.
+It has a type (using constants defined in the **ParamType** class) and a value. It
+has two implementations:
+
+- **ScalarParam** represents a single-valued parameter, such as a text string, a
+  number, or another message.
+- **ListParam** represents a list of values, which will be joined together with
+  appropriate separators. It has a "list type" (using constants defined in the
+  **ListType** class) defining the desired separators.
+
+### Formatters
+
+A formatter for a particular language is obtained from an implementation of
+**IMessageFormatterFactory**. No implementation of this interface is provided by
+this library. If an environment needs its formatters to vary behavior on things
+other than the language code, for example selecting among multiple sources of
+messages or markup language used for processing message texts, it should define
+a MessageFormatterFactoryFactory of some sort to provide appropriate
+IMessageFormatterFactory implementations.
+
+There is no one base interface for all formatters; the intent is that type
+hinting will ensure that the formatter being used will produce output in the
+expected output format. The defined output formats are:
+
+- **ITextFormatter** produces plain text output.
+
+No implementation of these interfaces are provided by this library.
+
+Formatter implementations are expected to perform the following procedure to
+generate the output string:
+
+1. Fetch the message's translation in the formatter's language. Details of this
+   fetching are unspecified here.
+   - If no translation is found in the formatter's language, it should attempt
+     to fall back to appropriate other languages. Details of the fallback are
+     unspecified here.
+   - If no translation can be found in any fallback language, a string should
+        be returned that indicates at minimum the message key that was unable to
+        be found.
+2. Replace placeholders with parameter values.
+   - Note that placeholders must not be replaced recursively. That is, if a
+     parameter's value contains text that looks like a placeholder, it must not
+     be replaced as if it really were a placeholder.
+   - Certain types of parameters are not substituted directly at this stage.
+     Instead their placeholders must be replaced with an opaque representation
+     that will not be misinterpreted during later stages.
+     - Parameters of type RAW or PLAINTEXT
+     - TEXT parameters with a MessageValue as the value
+     - LIST parameters with any late-substituted value as one of their values.
+3. Process any formatting commands.
+4. Process the source markup language to produce a string in the desired output
+   format. This may be a no-op, and may be combined with the previous step if
+   the markup language implements compatible formatting commands.
+5. Replace any opaque representations from step 2 with the actual values of
+   the corresponding parameters.
+
+Guidelines for Interoperability
+-------------------------------
+
+Besides allowing for libraries to safely supply their own translations for
+every app using them, and apps to easily use libraries' translations instead of
+having to retranslate everything, following these guidelines will also help
+open source projects use [translatewiki.net] for crowdsourced volunteer
+translation into many languages.
+
+### Language codes
+
+[BCP 47] language tags should be used for language codes. If a supplied
+language tag is not recognized, at minimum the corresponding tag with all
+optional subtags stripped should be tried as a fallback.
+
+All messages must have a translation in English (code "en"). All languages
+should fall back to English as a last resort.
+
+The English translations should use `{{PLURAL:...}}` and `{{GENDER:...}}` even
+when English doesn't make a grammatical distinction, to signal to translators
+that plural/gender support is available.
+
+Language code "qqq" is reserved for documenting messages. Documentation should
+describe the context in which the message is used and the values of all
+parameters used with the message. Generally this is written in English.
+Attempting to obtain a message formatter for "qqq" should return one for "en"
+instead.
+
+Language code "qqx" is reserved for debugging. Rather than retrieving
+translations from some underlying storage, every key should act as if it were
+translated as something `(key-name: $1, $2, $3)` with the number of
+placeholders depending on how many parameters are included in the
+MessageValue.
+
+### Message keys
+
+Message keys intended for use with external implementations should follow
+certain guidelines for interoperability:
+
+- Keys should be restricted to the regular expression `/^[a-z][a-z0-9-]*$/`.
+  That is, it should consist of lowercase ASCII letters, numbers, and hyphen
+  only, and should begin with a letter.
+- Keys should be prefixed to help avoid collisions. For example, a library
+  named "ApplePicker" should prefix its message keys with "applepicker-".
+- Common values needing translation, such as names of months and weekdays,
+  should not be prefixed by each library. Libraries needing these should use
+  keys from the [Common Locale Data Repository][CLDR] and document this
+  requirement, and environments should provide these messages.
+
+### Message format
+
+Placeholders are represented by `$1`, `$2`, `$3`, and so on. Text like `$100`
+is interpreted as a placeholder for parameter 100 if 100 or more parameters
+were supplied, as a placeholder for parameter 10 followed by text "0" if
+between ten and 99 parameters were supplied, and as a placeholder for parameter
+1 followed by text "00" if between one and nine parameters were supplied.
+
+All formatting commands look like `{{NAME:$value1|$value2|$value3|...}}`. Braces
+are to be balanced, e.g. `{{NAME:foo|{{bar|baz}}}}` has $value1 as "foo" and
+$value2 as "{{bar|baz}}". The name is always case-insensitive.
+
+Anything syntactically resembling a placeholder or formatting command that does
+not correspond to an actual paramter or known command should be left unchanged
+for processing by the markup language processor.
+
+Libraries providing messages for use by externally-defined formatters should
+generally assume no markup language will be applied, and should avoid
+constructs used by common markup languages unless they also make sense when
+read as plain text.
+
+### Formatting commands
+
+The following formatting commands should be supported.
+
+#### PLURAL
+
+`{{PLURAL:$count|$formA|$formB|...}}` is used to produce plurals.
+
+$count is a number, which may have been formatted with ParamType::NUM.
+
+The number of forms and which count corresponds to which form depend on the
+language, for example English uses `{{PLURAL:$1|one|other}}` while Arabic uses
+`{{PLURAL:$1|zero|one|two|few|many|other}}`. Details are defined in
+[CLDR][CLDR plurals].
+
+It is not possible to "skip" positions while still suppling later ones. If too
+few values are supplied, the final form is repeated for subsequent positions.
+
+If there is an explicit plural form to be given for a specific number, it may
+be specified with syntax like `{{PLURAL:$1|one egg|$1 eggs|12=a dozen eggs}}`.
+
+#### GENDER
+
+`{{GENDER:$name|$masculine|$feminine|$unspecified}}` is used to handle
+grammatical gender, typically when messages refer to user accounts.
+
+This supports three grammatical genders: "male", "female", and a third option
+for cases where the gender is unspecified, unknown, or neither male nor female.
+It does not attempt to handle animate-inanimate or [T-V] distinctions.
+
+$name is a user account name or other similar identifier. If the name given
+does not correspond to any known user account, it should probably use the
+$unspecified gender.
+
+If $feminine and/or $unspecified is not specified, the value of $masculine
+is normally used in its place.
+
+#### GRAMMAR
+
+`{{GRAMMAR:$form|$term}}` converts a term to an appropriate grammatical form.
+
+If no mapping for $term to $form exists, $term should be returned unchanged.
+
+See [jQuery.i18n § Grammar][jQuery.i18n grammar] for details.
+
+#### BIDI
+
+`{{BIDI:$text}}` applies directional isolation to the wrapped text, to attempt
+to avoid errors where directionally-neutral characters are wrongly displayed
+when between LTR and RTL content.
+
+This should output U+202A (left-to-right embedding) or U+202B (right-to-left
+embedding) before the text, depending on the directionality of the first
+strongly-directional character in $text, and U+202C (pop directional
+formatting) after, or do something equivalent for the target output format.
+
+### Supplying translations
+
+Code intending its messages to be used by externally-defined formatters should
+supply the translations as described by
+[jQuery.i18n § Message File Format][jQuery.i18n file format].
+
+In brief, the base directory of the library should contain a directory named
+"i18n". This directory should contain JSON files named by code such as
+"en.json", "de.json", "qqq.json", each with contents like:
+
+```json
+{
+    "@metadata": {
+        "authors": [
+            "Alice",
+            "Bob",
+            "Carol",
+            "David"
+        ],
+        "last-updated": "2012-09-21"
+    },
+    "appname-title": "Example Application",
+    "appname-sub-title": "An example application",
+    "appname-header-introduction": "Introduction",
+    "appname-about": "About this application",
+    "appname-footer": "Footer text"
+}
+```
+
+Formatter implementations should be able to consume message data supplied in
+this format, either directly via registration of i18n directories to check or
+by providing tooling to incorporate it during a build step.
+
+
+---
+[jQuery.i18n]: https://github.com/wikimedia/jquery.i18n
+[BCP 47]: https://tools.ietf.org/rfc/bcp/bcp47.txt
+[CLDR]: http://cldr.unicode.org/
+[CLDR plurals]: https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
+[jQuery.i18n grammar]: https://github.com/wikimedia/jquery.i18n#grammar
+[jQuery.i18n file format]: https://github.com/wikimedia/jquery.i18n#message-file-format
+[translatewiki.net]: https://translatewiki.net/wiki/Translating:New_project
+[T-V]: https://en.wikipedia.org/wiki/T%E2%80%93V_distinction
diff --git a/includes/libs/Message/ScalarParam.php b/includes/libs/Message/ScalarParam.php
new file mode 100644 (file)
index 0000000..c17bc7f
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace Wikimedia\Message;
+
+/**
+ * Value object representing a message parameter holding a single value.
+ *
+ * Message parameter classes are pure value objects and are safely newable.
+ */
+class ScalarParam extends MessageParam {
+       /**
+        * Construct a text parameter
+        *
+        * @param string $type One of the ParamType constants.
+        * @param string|int|float|MessageValue $value
+        */
+       public function __construct( $type, $value ) {
+               $this->type = $type;
+               $this->value = $value;
+       }
+
+       public function dump() {
+               if ( $this->value instanceof MessageValue ) {
+                       $contents = $this->value->dump();
+               } else {
+                       $contents = htmlspecialchars( $this->value );
+               }
+               return "<{$this->type}>" . $contents . "</{$this->type}>";
+       }
+}
diff --git a/includes/libs/Message/TextParam.php b/includes/libs/Message/TextParam.php
deleted file mode 100644 (file)
index c1a1f08..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?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}>";
-       }
-}
index 233810f..74c56a0 100644 (file)
@@ -7,13 +7,13 @@ use MediaWikiTestCase;
 use Message;
 use Wikimedia\Message\MessageValue;
 use Wikimedia\Message\ParamType;
-use Wikimedia\Message\TextParam;
+use Wikimedia\Message\ScalarParam;
 
 /**
  * @covers \MediaWiki\Message\TextFormatter
  * @covers \Wikimedia\Message\MessageValue
  * @covers \Wikimedia\Message\ListParam
- * @covers \Wikimedia\Message\TextParam
+ * @covers \Wikimedia\Message\ScalarParam
  * @covers \Wikimedia\Message\MessageParam
  */
 class TextFormatterTest extends MediaWikiTestCase {
@@ -45,7 +45,7 @@ class TextFormatterTest extends MediaWikiTestCase {
                $formatter = $this->createTextFormatter( 'en' );
                $mv = ( new MessageValue( 'test' ) )->commaListParams( [
                        'a',
-                       new TextParam( ParamType::BITRATE, 100 ),
+                       new ScalarParam( ParamType::BITRATE, 100 ),
                ] );
                $result = $formatter->format( $mv );
                $this->assertSame( 'test a, 100 bps $2', $result );
index 04dfa4e..f0205b8 100644 (file)
@@ -5,13 +5,13 @@ namespace Wikimedia\Tests\Message;
 use Wikimedia\Message\ListType;
 use Wikimedia\Message\MessageValue;
 use Wikimedia\Message\ParamType;
-use Wikimedia\Message\TextParam;
+use Wikimedia\Message\ScalarParam;
 use MediaWikiTestCase;
 
 /**
  * @covers \Wikimedia\Message\MessageValue
  * @covers \Wikimedia\Message\ListParam
- * @covers \Wikimedia\Message\TextParam
+ * @covers \Wikimedia\Message\ScalarParam
  * @covers \Wikimedia\Message\MessageParam
  */
 class MessageValueTest extends MediaWikiTestCase {
@@ -26,7 +26,7 @@ class MessageValueTest extends MediaWikiTestCase {
                                '<message key="key"><text>a</text></message>'
                        ],
                        [
-                               [ new TextParam( ParamType::BITRATE, 100 ) ],
+                               [ new ScalarParam( ParamType::BITRATE, 100 ) ],
                                '<message key="key"><bitrate>100</bitrate></message>'
                        ],
                ];
@@ -46,7 +46,7 @@ class MessageValueTest extends MediaWikiTestCase {
        public function testParams() {
                $mv = new MessageValue( 'key' );
                $mv->params( 1, 'x' );
-               $mv2 = $mv->params( new TextParam( ParamType::BITRATE, 100 ) );
+               $mv2 = $mv->params( new ScalarParam( ParamType::BITRATE, 100 ) );
                $this->assertSame(
                        '<message key="key"><text>1</text><text>x</text><bitrate>100</bitrate></message>',
                        $mv->dump() );
@@ -76,10 +76,11 @@ class MessageValueTest extends MediaWikiTestCase {
 
        public function testTextParams() {
                $mv = new MessageValue( 'key' );
-               $mv2 = $mv->textParams( 'a', 'b' );
+               $mv2 = $mv->textParams( 'a', 'b', new MessageValue( 'key2' ) );
                $this->assertSame( '<message key="key">' .
                        '<text>a</text>' .
                        '<text>b</text>' .
+                       '<text><message key="key2"></message></text>' .
                        '</message>',
                        $mv->dump() );
                $this->assertSame( $mv, $mv2 );