HTMLForm: Allow i18n of 'options'
authorBrad Jorsch <bjorsch@wikimedia.org>
Tue, 18 Feb 2014 20:28:06 +0000 (15:28 -0500)
committerReedy <reedy@wikimedia.org>
Wed, 19 Feb 2014 23:26:23 +0000 (23:26 +0000)
One shortcoming in HTMLForm is that fields that use the 'options'
parameter (e.g. <select>) have no easy way for the individual labels to
be localized.

This change adds a new parameter type, 'options-messages', where the
keys are message keys rather than bare strings (similar to the
difference between 'label' and 'label-message'). It also abstracts out
the fetching of the various option parameters, and changes the necessary
field classes to use it.

Change-Id: If4175332405d26c7ff2e8fbe100bcad61762ce6f

RELEASE-NOTES-1.23
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/htmlform/HTMLMultiSelectField.php
includes/htmlform/HTMLRadioField.php
includes/htmlform/HTMLSelectAndOtherField.php
includes/htmlform/HTMLSelectField.php
includes/htmlform/HTMLSelectOrOtherField.php

index 9664f1f..64c2b3b 100644 (file)
@@ -102,6 +102,9 @@ production.
    $wgDBWindowsAuthentication, which makes the web server authenticate against
    the database server using Integrated Windows Authentication instead of
    $wgDBuser/$wgDBpassword.
+* HTMLForm 'select', 'selectandother', 'selectorother', 'multiselect', and
+  'radio' fields can now use message keys as labels via the 'options-messages'
+  parameter, which overrides the 'options' parameter.
 
 === Bug fixes in 1.23 ===
 * (bug 41759) The "updated since last visit" markers (on history pages, recent
index 43c1a94..3e2a09e 100644 (file)
  *    'default'             -- default value when the form is displayed
  *    'id'                  -- HTML id attribute
  *    'cssclass'            -- CSS class
- *    'options'             -- varies according to the specific object.
+ *    'options'             -- associative array mapping labels to values.
+ *                             Some field types support multi-level arrays.
+ *    'options-messages'    -- associative array mapping message keys to values.
+ *                             Some field types support multi-level arrays.
+ *    'options-message'     -- message key to be parsed to extract the list of
+ *                             options (like 'ipbreason-dropdown').
  *    'label-message'       -- message key for a message to use as the label.
  *                             can be an array of msg key and then parameters to
  *                             the message.
index dcdba1e..27d9594 100644 (file)
@@ -14,6 +14,8 @@ abstract class HTMLFormField {
        protected $mID;
        protected $mClass = '';
        protected $mDefault;
+       protected $mOptions = false;
+       protected $mOptionsLabelsNotFromMessage = false;
 
        /**
         * @var bool If true will generate an empty div element with no label
@@ -508,6 +510,88 @@ abstract class HTMLFormField {
                return $ret;
        }
 
+       /**
+        * Given an array of msg-key => value mappings, returns an array with keys
+        * being the message texts. It also forces values to strings.
+        *
+        * @param array $options
+        * @return array
+        */
+       private function lookupOptionsKeys( $options ) {
+               $ret = array();
+               foreach ( $options as $key => $value ) {
+                       $key = $this->msg( $key )->plain();
+                       $ret[$key] = is_array( $value )
+                               ? $this->lookupOptionsKeys($field, $value)
+                               : strval( $value );
+               }
+               return $ret;
+       }
+
+       /**
+        * Recursively forces values in an array to strings, because issues arise
+        * with integer 0 as a value.
+        *
+        * @param array $array
+        * @return array
+        */
+       static function forceToStringRecursive( $array ) {
+               if ( is_array( $array ) ) {
+                       return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array );
+               } else {
+                       return strval( $array );
+               }
+       }
+
+       /**
+        * Fetch the array of options from the field's parameters. In order, this
+        * checks 'options-messages', 'options', then 'options-message'.
+        *
+        * @return array|null Options array
+        */
+       public function getOptions() {
+               if ( $this->mOptions === false ) {
+                       if ( array_key_exists( 'options-messages', $this->mParams ) ) {
+                               $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'] );
+                       } elseif ( array_key_exists( 'options', $this->mParams ) ) {
+                               $this->mOptionsLabelsNotFromMessage = true;
+                               $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
+                       } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
+                               /** @todo This is copied from Xml::listDropDown(), deprecate/avoid duplication? */
+                               $message = $this->msg( $this->mParams['options-message'] )->plain();
+
+                               $optgroup = false;
+                               $this->mOptions = array();
+                               foreach ( explode( "\n", $message ) as $option ) {
+                                       $value = trim( $option );
+                                       if ( $value == '' ) {
+                                               continue;
+                                       } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
+                                               # A new group is starting...
+                                               $value = trim( substr( $value, 1 ) );
+                                               $optgroup = $value;
+                                       } elseif ( substr( $value, 0, 2 ) == '**' ) {
+                                               # groupmember
+                                               $opt = trim( substr( $value, 2 ) );
+                                               if ( $optgroup === false ) {
+                                                       $this->mOptions[$opt] = $opt;
+                                               } else {
+                                                       $this->mOptions[$optgroup][$opt] = $opt;
+                                               }
+                                       } else {
+                                               # groupless reason list
+                                               $optgroup = false;
+                                               $this->mOptions[$option] = $option;
+                                       }
+                               }
+                       } else {
+                               $this->mOptions = null;
+                       }
+               }
+
+               return $this->mOptions;
+       }
+
        /**
         * flatten an array of options to a single array, for instance,
         * a set of "<options>" inside "<optgroups>".
index 09576d4..2944c24 100644 (file)
@@ -17,7 +17,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
 
                # If all options are valid, array_intersect of the valid options
                # and the provided options will return the provided options.
-               $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
+               $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
 
                $validValues = array_intersect( $value, $validOptions );
                if ( count( $validValues ) == count( $value ) ) {
@@ -28,7 +28,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        }
 
        function getInputHTML( $value ) {
-               $html = $this->formatOptions( $this->mParams['options'], $value );
+               $html = $this->formatOptions( $this->getOptions(), $value );
 
                return $html;
        }
@@ -37,6 +37,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
                $html = '';
 
                $attribs = $this->getAttributes( array( 'disabled', 'tabindex' ) );
+               $elementFunc = array( 'Html', $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' );
 
                foreach ( $options as $label => $info ) {
                        if ( is_array( $info ) ) {
@@ -50,7 +51,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
                                        in_array( $info, $value, true ),
                                        $attribs + $thisAttribs
                                );
-                               $checkbox .= '&#160;' . Html::rawElement(
+                               $checkbox .= '&#160;' . call_user_func( $elementFunc,
                                        'label',
                                        array( 'for' => "{$this->mID}-$info" ),
                                        $label
@@ -102,7 +103,7 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        }
 
        function filterDataForSubmit( $data ) {
-               $options = HTMLFormField::flattenOptions( $this->mParams['options'] );
+               $options = HTMLFormField::flattenOptions( $this->getOptions() );
 
                $res = array();
                foreach ( $options as $opt ) {
index f206ed6..aa0ea5d 100644 (file)
@@ -15,7 +15,7 @@ class HTMLRadioField extends HTMLFormField {
                        return false;
                }
 
-               $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
+               $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
 
                if ( in_array( $value, $validOptions ) ) {
                        return true;
@@ -33,7 +33,7 @@ class HTMLRadioField extends HTMLFormField {
         * @return String
         */
        function getInputHTML( $value ) {
-               $html = $this->formatOptions( $this->mParams['options'], $value );
+               $html = $this->formatOptions( $this->getOptions(), $value );
 
                return $html;
        }
@@ -42,6 +42,7 @@ class HTMLRadioField extends HTMLFormField {
                $html = '';
 
                $attribs = $this->getAttributes( array( 'disabled', 'tabindex' ) );
+               $elementFunc = array( 'Html', $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' );
 
                # @todo Should this produce an unordered list perhaps?
                foreach ( $options as $label => $info ) {
@@ -51,7 +52,7 @@ class HTMLRadioField extends HTMLFormField {
                        } else {
                                $id = Sanitizer::escapeId( $this->mID . "-$info" );
                                $radio = Xml::radio( $this->mName, $info, $info == $value, $attribs + array( 'id' => $id ) );
-                               $radio .= '&#160;' . Html::rawElement( 'label', array( 'for' => $id ), $label );
+                               $radio .= '&#160;' . call_user_func( $elementFunc, 'label', array( 'for' => $id ), $label );
 
                                $html .= ' ' . Html::rawElement(
                                        'div',
index 044670d..2e91a23 100644 (file)
@@ -19,63 +19,17 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
                        $params['other'] = null;
                }
 
-               if ( array_key_exists( 'options', $params ) ) {
-                       # Options array already specified
-               } elseif ( array_key_exists( 'options-message', $params ) ) {
-                       # Generate options array from a system message
-                       $params['options'] =
-                               self::parseMessage( wfMessage( $params['options-message'] )->inContentLanguage()->plain(),
-                                       $params['other'] );
-               } else {
-                       # Sulk
-                       throw new MWException( 'HTMLSelectAndOtherField called without any options' );
-               }
-               $this->mFlatOptions = self::flattenOptions( $params['options'] );
-
                parent::__construct( $params );
-       }
 
-       /**
-        * Build a drop-down box from a textual list.
-        *
-        * @param string $string message text
-        * @param string $otherName name of "other reason" option
-        *
-        * @return Array
-        * @todo This is copied from Xml::listDropDown(), deprecate/avoid duplication?
-        */
-       public static function parseMessage( $string, $otherName = null ) {
-               if ( $otherName === null ) {
-                       $otherName = wfMessage( 'htmlform-selectorother-other' )->plain();
+               if ( $this->getOptions() === null ) {
+                       # Sulk
+                       throw new MWException( 'HTMLSelectAndOtherField called without any options' );
                }
-
-               $optgroup = false;
-               $options = array( $otherName => 'other' );
-
-               foreach ( explode( "\n", $string ) as $option ) {
-                       $value = trim( $option );
-                       if ( $value == '' ) {
-                               continue;
-                       } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
-                               # A new group is starting...
-                               $value = trim( substr( $value, 1 ) );
-                               $optgroup = $value;
-                       } elseif ( substr( $value, 0, 2 ) == '**' ) {
-                               # groupmember
-                               $opt = trim( substr( $value, 2 ) );
-                               if ( $optgroup === false ) {
-                                       $options[$opt] = $opt;
-                               } else {
-                                       $options[$optgroup][$opt] = $opt;
-                               }
-                       } else {
-                               # groupless reason list
-                               $optgroup = false;
-                               $options[$option] = $option;
-                       }
+               if ( !in_array( 'other', $this->mOptions, true ) ) {
+                       $this->mOptions[$params['other']] = 'other';
                }
+               $this->mFlatOptions = self::flattenOptions( $this->getOptions() );
 
-               return $options;
        }
 
        function getInputHTML( $value ) {
index 9d719f6..0437480 100644 (file)
@@ -11,7 +11,7 @@ class HTMLSelectField extends HTMLFormField {
                        return $p;
                }
 
-               $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
+               $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
 
                if ( in_array( $value, $validOptions ) ) {
                        return true;
@@ -23,16 +23,6 @@ class HTMLSelectField extends HTMLFormField {
        function getInputHTML( $value ) {
                $select = new XmlSelect( $this->mName, $this->mID, strval( $value ) );
 
-               # If one of the options' 'name' is int(0), it is automatically selected.
-               # because PHP sucks and thinks int(0) == 'some string'.
-               # Working around this by forcing all of them to strings.
-               foreach ( $this->mParams['options'] as &$opt ) {
-                       if ( is_int( $opt ) ) {
-                               $opt = strval( $opt );
-                       }
-               }
-               unset( $opt ); # PHP keeps $opt around as a reference, which is a bit scary
-
                if ( !empty( $this->mParams['disabled'] ) ) {
                        $select->setAttribute( 'disabled', 'disabled' );
                }
@@ -45,7 +35,7 @@ class HTMLSelectField extends HTMLFormField {
                        $select->setAttribute( 'class', $this->mClass );
                }
 
-               $select->addOptions( $this->mParams['options'] );
+               $select->addOptions( $this->getOptions() );
 
                return $select->getHTML();
        }
index 08be435..045b8df 100644 (file)
@@ -5,38 +5,29 @@
  */
 class HTMLSelectOrOtherField extends HTMLTextField {
        function __construct( $params ) {
-               if ( !in_array( 'other', $params['options'], true ) ) {
+               parent::__construct( $params );
+               $this->getOptions();
+               if ( !in_array( 'other', $this->mOptions, true ) ) {
                        $msg =
                                isset( $params['other'] )
                                        ? $params['other']
                                        : wfMessage( 'htmlform-selectorother-other' )->text();
-                       $params['options'][$msg] = 'other';
+                       $this->mOptions[$msg] = 'other';
                }
 
-               parent::__construct( $params );
-       }
-
-       static function forceToStringRecursive( $array ) {
-               if ( is_array( $array ) ) {
-                       return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array );
-               } else {
-                       return strval( $array );
-               }
        }
 
        function getInputHTML( $value ) {
                $valInSelect = false;
 
                if ( $value !== false ) {
-                       $valInSelect = in_array( $value, HTMLFormField::flattenOptions( $this->mParams['options'] ) );
+                       $valInSelect = in_array( $value, HTMLFormField::flattenOptions( $this->getOptions() ) );
                }
 
                $selected = $valInSelect ? $value : 'other';
 
-               $opts = self::forceToStringRecursive( $this->mParams['options'] );
-
                $select = new XmlSelect( $this->mName, $this->mID, $selected );
-               $select->addOptions( $opts );
+               $select->addOptions( $this->getOptions() );
 
                $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );