Merge "Disable styling for checkboxes and radios on non-js browsers"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 14 Jan 2015 20:54:39 +0000 (20:54 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 14 Jan 2015 20:54:39 +0000 (20:54 +0000)
48 files changed:
Gruntfile.js [new file with mode: 0644]
RELEASE-NOTES-1.25
autoload.php
includes/api/ApiMain.php
includes/api/i18n/be-tarask.json
includes/api/i18n/cs.json
includes/api/i18n/es.json
includes/deferred/LinksUpdate.php
includes/deferred/SqlDataUpdate.php
includes/htmlform/HTMLCheckField.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/htmlform/HTMLFormFieldCloner.php
includes/htmlform/VFormHTMLForm.php [new file with mode: 0644]
includes/installer/Installer.php
includes/installer/WebInstallerPage.php
includes/installer/i18n/en.json
includes/installer/i18n/qqq.json
includes/page/ImagePage.php
includes/specialpage/FormSpecialPage.php
includes/specials/SpecialChangeEmail.php
includes/specials/SpecialJavaScriptTest.php
includes/specials/SpecialPageLanguage.php
includes/specials/SpecialPasswordReset.php
includes/specials/SpecialUpload.php
languages/i18n/az.json
languages/i18n/be-tarask.json
languages/i18n/bg.json
languages/i18n/ca.json
languages/i18n/de.json
languages/i18n/es.json
languages/i18n/fr.json
languages/i18n/he.json
languages/i18n/ia.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/pl.json
languages/i18n/pt.json
languages/i18n/sv.json
languages/i18n/yue.json
maintenance/jsduck/eg-iframe.html
package.json [new file with mode: 0644]
resources/Resources.php
resources/src/mediawiki.action/mediawiki.action.view.redirect.js
resources/src/mediawiki/mediawiki.js
tests/frontend/Gruntfile.js [deleted file]
tests/frontend/package.json [deleted file]
tests/qunit/suites/resources/mediawiki/mediawiki.test.js

diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644 (file)
index 0000000..9cf89d0
--- /dev/null
@@ -0,0 +1,111 @@
+/*jshint node:true */
+module.exports = function ( grunt ) {
+       grunt.loadNpmTasks( 'grunt-contrib-jshint' );
+       grunt.loadNpmTasks( 'grunt-contrib-watch' );
+       grunt.loadNpmTasks( 'grunt-banana-checker' );
+       grunt.loadNpmTasks( 'grunt-jscs' );
+       grunt.loadNpmTasks( 'grunt-jsonlint' );
+       grunt.loadNpmTasks( 'grunt-karma' );
+
+       var wgServer = process.env.MW_SERVER,
+               wgScriptPath = process.env.MW_SCRIPT_PATH;
+
+       grunt.initConfig( {
+               pkg: grunt.file.readJSON( 'package.json' ),
+               jshint: {
+                       options: {
+                               jshintrc: true
+                       },
+                       all: [
+                               '*.js',
+                               '{includes,languages,resources,skins,tests}/**/*.js'
+                       ]
+               },
+               jscs: {
+                       all: [
+                               '<%= jshint.all %>',
+                               // Auto-generated file with JSON (double quotes)
+                               '!tests/qunit/data/mediawiki.jqueryMsg.data.js',
+                               // Skip functions are stored as script files but wrapped in a function when
+                               // executed. node-jscs trips on the would-be "Illegal return statement".
+                               '!resources/src/*-skip.js'
+
+                       // Exclude all files ignored by jshint
+                       ].concat( grunt.file.read( '.jshintignore' ).split( '\n' ).reduce( function ( patterns, pattern ) {
+                               // Filter out empty lines
+                               if ( pattern.length && pattern[0] !== '#' ) {
+                                       patterns.push( '!' + pattern );
+                               }
+                               return patterns;
+                       }, [] ) )
+               },
+               jsonlint: {
+                       all: [
+                               '.jscsrc',
+                               '{languages,maintenance,resources}/**/*.json',
+                               'package.json'
+                       ]
+               },
+               banana: {
+                       core: 'languages/i18n/',
+                       api: 'includes/api/i18n/',
+                       installer: 'includes/installer/i18n/'
+               },
+               watch: {
+                       files: [
+                               '<%= jscs.all %>',
+                               '<%= jsonlint.all %>',
+                               '.jshintignore',
+                               '.jshintrc'
+                       ],
+                       tasks: 'test'
+               },
+               karma: {
+                       options: {
+                               proxies: ( function () {
+                                       var obj = {};
+                                       // Set up a proxy for requests to relative urls inside wgScriptPath. Uses a
+                                       // property accessor instead of plain obj[wgScriptPath] assignment as throw if
+                                       // unset. Running grunt normally (e.g. npm test), should not fail over this.
+                                       // This ensures 'npm test' works out of the box, statically, on a git clone
+                                       // without MediaWiki fully installed or some environment variables set.
+                                       Object.defineProperty( obj, wgScriptPath, {
+                                               enumerable: true,
+                                               get: function () {
+                                                       if ( !wgServer ) {
+                                                               grunt.fail.fatal( 'MW_SERVER is not set' );
+                                                       }
+                                                       if ( !wgScriptPath ) {
+                                                               grunt.fail.fatal( 'MW_SCRIPT_PATH is not set' );
+                                                       }
+                                                       return wgServer + wgScriptPath;
+                                               }
+                                       } );
+                                       return obj;
+                               }() ),
+                               files: [ {
+                                       pattern: wgServer + wgScriptPath + '/index.php?title=Special:JavaScriptTest/qunit/export',
+                                       watched: false,
+                                       included: true,
+                                       served: false
+                               } ],
+                               frameworks: [ 'qunit' ],
+                               reporters: [ 'dots' ],
+                               singleRun: true,
+                               autoWatch: false
+                       },
+                       main: {
+                               browsers: [ 'Chrome' ]
+                       },
+                       more: {
+                               browsers: [ 'Chrome', 'Firefox' ]
+                       }
+               }
+       } );
+
+       grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] );
+       grunt.registerTask( 'qunit', 'karma:main' );
+
+       grunt.registerTask( 'test', ['lint'] );
+       grunt.registerTask( 'default', 'test' );
+};
index 2aa066c..76295a9 100644 (file)
@@ -306,6 +306,16 @@ changes to languages because of Bugzilla reports.
   rather than as strings that must be prepended or appended to $comment.
 * (T30950, T31025) RFC, PMID, and ISBN "magic links" can no longer contain
   newlines; but they can contain &nbsp; and other non-newline whitespace.
+* The 'mediawiki.action.edit' ResourceLoader module no longer generates the edit
+  toolbar, which has been moved to a separate 'mediawiki.toolbar' module. If you
+  relied on this behavior, update your scripts' dependencies.
+* HTMLForm's 'vform' display style has been separated to a subclass. Therefore:
+  * HTMLForm::isVForm() is now deprecated.
+  * You can no longer do this:
+      $form = new HTMLForm( … );
+      $form->setDisplayFormat( 'vform' ); // throws exception
+    Instead, do this:
+      $form = HTMLForm::factory( 'vform', … );
 
 == Compatibility ==
 
index 72345fe..46c8b01 100644 (file)
@@ -1269,6 +1269,7 @@ $wgAutoloadLocalClasses = array(
        'UsersPager' => __DIR__ . '/includes/specials/SpecialListusers.php',
        'UtfNormal' => __DIR__ . '/includes/normal/UtfNormal.php',
        'UzConverter' => __DIR__ . '/languages/classes/LanguageUz.php',
+       'VFormHTMLForm' => __DIR__ . '/includes/htmlform/VFormHTMLForm.php',
        'ValidateRegistrationFile' => __DIR__ . '/maintenance/validateRegistrationFile.php',
        'ViewAction' => __DIR__ . '/includes/actions/ViewAction.php',
        'VirtualRESTService' => __DIR__ . '/includes/libs/virtualrest/VirtualRESTService.php',
index 82ed295..9a98054 100644 (file)
@@ -554,6 +554,7 @@ class ApiMain extends ApiBase {
 
                        $response->header( "Access-Control-Allow-Origin: $originHeader" );
                        $response->header( 'Access-Control-Allow-Credentials: true' );
+                       $response->header( "Timing-Allow-Origin: $originHeader" ); # http://www.w3.org/TR/resource-timing/#timing-allow-origin
 
                        if ( !$preflight ) {
                                $response->header( 'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag' );
index fd08c51..93f4b23 100644 (file)
@@ -46,5 +46,6 @@
        "apihelp-createaccount-param-domain": "Дамэн для вонкавай аўтэнтыфікацыі (неабавязкова).",
        "apihelp-createaccount-param-token": "Маркер стварэньня рахунку, атрыманы пры першым запыце.",
        "apihelp-createaccount-param-email": "Адрас электроннай пошты ўдзельніка (неабавязкова).",
-       "apihelp-createaccount-param-realname": "Сапраўднае імя ўдзельніка (неабавязкова)."
+       "apihelp-createaccount-param-realname": "Сапраўднае імя ўдзельніка (неабавязкова).",
+       "apihelp-createaccount-param-mailpassword": "Калі ўсталяванае любое значэньне, выпадковы пароль будзе дасланы карыстальніку на электронную пошту."
 }
index 9e6b254..14fae0c 100644 (file)
        "apihelp-block-param-anononly": "Zablokovat pouze anonymní uživatele (tj. zakázat editovat anonymně z této IP).",
        "apihelp-block-param-nocreate": "Nedovolit registraci nových uživatelů.",
        "apihelp-block-param-noemail": "Zakázat uživateli posílat e-maily prostřednictvím wiki. (Vyžaduje oprávnění „blockemail“.)",
-       "apihelp-block-param-hidename": "Skryj uživatelské jméno v logu zablokování. (K tomu jsou potřeba práva \"hideuser\").",
+       "apihelp-block-param-hidename": "Skrýt uživatelské jméno v knize zablokování. (Vyžaduje oprávnění „hideuser“.)",
        "apihelp-block-param-allowusertalk": "Povolit uživateli editovat svou vlastní diskusní stránku (závisí na $wgBlockAllowsUTEdit).",
        "apihelp-block-param-reblock": "Pokud již uživatel blokován je, přepsat současný blok.",
        "apihelp-block-param-watchuser": "Sledovat uživatelskou a diskusní stranu tohoto uživatele nebo adresy IP.",
-       "apihelp-block-example-ip-simple": "Zablokuj IP 192.0.2.5 na tři dny s důvodem \"První útok\"",
+       "apihelp-block-example-ip-simple": "Zablokovat IP 192.0.2.5 na tři dny s důvodem „First strike“",
        "apihelp-block-example-user-complex": "Trvale zablokovat uživatele Vandal s odůvodněním „Vandalism“ a bránit vytváření nových účtů a e-mailování",
        "apihelp-compare-description": "Vrátí rozdíl dvou stránek.\n\nVe „from“ a „to“ musíte zadat číslo revize, název stránky nebo ID stránky.",
        "apihelp-compare-param-fromtitle": "Název první stránky k porovnání.",
        "apihelp-delete-description": "Smazat stránku.",
        "apihelp-edit-param-minor": "Malá editace.",
        "apihelp-edit-param-notminor": "Nemalá editace.",
-       "apihelp-edit-param-bot": "Označ tuto editaci jako editaci bota.",
-       "apihelp-edit-param-createonly": "Needituj stráku, pokud již existuje.",
-       "apihelp-edit-param-watch": "Vlož stránku na seznam sledovaných stránek.",
-       "apihelp-edit-param-unwatch": "Odstraň stránku ze svého seznamu sledovaných stránek.",
+       "apihelp-edit-param-bot": "Označit tuto editaci jako editaci bota.",
+       "apihelp-edit-param-createonly": "Needitovat stránku, pokud již existuje.",
+       "apihelp-edit-param-watch": "Přidat stránku na váš seznam sledovaných stránek.",
+       "apihelp-edit-param-unwatch": "Odstranit stránku z vašeho seznamu sledovaných stránek.",
        "apihelp-help-description": "Zobrazuje nápovědu k uvedeným modulům.",
        "apihelp-help-param-modules": "Moduly, pro které se má zobrazit nápověda (hodnoty parametrů action= a format= nebo „main“). Submoduly lze zadávat pomocí „+“.",
        "apihelp-help-param-submodules": "Zahrnout nápovědu pro podmoduly uvedeného modulu.",
index 4550a75..7964b15 100644 (file)
@@ -18,6 +18,9 @@
        "apihelp-compare-param-fromtitle": "Primer título para comparar",
        "apihelp-createaccount-description": "Crear una nueva cuenta de usuario.",
        "apihelp-createaccount-param-name": "Nombre de usuario.",
+       "apihelp-createaccount-param-email": "Dirección de correo electrónico del usuario (opcional).",
+       "apihelp-createaccount-param-realname": "Nombre verdadero del usuario (opcional).",
+       "apihelp-createaccount-example-pass": "Crear cuenta de usuario «testuser» con la contraseña «test123»",
        "apihelp-delete-description": "Borrar una página.",
        "apihelp-delete-param-watch": "Añadir esta página a tu lista de seguimiento.",
        "apihelp-delete-param-unwatch": "Borrar esta página de tu lista de seguimiento.",
        "apihelp-edit-param-notminor": "Edición no menor.",
        "apihelp-edit-param-bot": "Marcar esta edición como de bot.",
        "apihelp-edit-param-createonly": "No editar la página si ya existe.",
+       "apihelp-edit-param-nocreate": "Producir un error si la página no existe.",
        "apihelp-edit-param-watch": "Añadir la página a tu lista de seguimiento.",
        "apihelp-edit-param-unwatch": "Quitar la página de tu lista de seguimiento.",
        "apihelp-edit-example-edit": "Editar una página",
+       "apihelp-edit-example-prepend": "Anteponer _&#95;NOTOC_&#95; a una página",
+       "apihelp-edit-example-undo": "Deshacer intervalo de revisiones 13579-13585 con resumen automático",
        "apihelp-emailuser-description": "Enviar un mensaje de correo electrónico a un usuario.",
+       "apihelp-emailuser-param-target": "Cuenta de usuario destinatario.",
+       "apihelp-emailuser-param-subject": "Encabezamiento de asunto.",
+       "apihelp-emailuser-param-text": "Cuerpo del mensaje.",
+       "apihelp-emailuser-param-ccme": "Enviarme una copia de este mensaje.",
        "apihelp-expandtemplates-param-title": "Título de la página.",
        "apihelp-expandtemplates-param-text": "Sintaxis wiki que se convertirá.",
        "apihelp-feedcontributions-description": "Devuelve el canal de contribuciones de un usuario.",
        "apihelp-feedcontributions-param-year": "A partir del año (y anteriores).",
        "apihelp-feedcontributions-param-month": "A partir del mes (y anteriores).",
        "apihelp-feedcontributions-param-deletedonly": "Mostrar solo las contribuciones borradas.",
+       "apihelp-feedrecentchanges-param-feedformat": "El formato del canal.",
+       "apihelp-feedrecentchanges-param-from": "Mostrar los cambios realizados a partir de entonces.",
        "apihelp-feedrecentchanges-param-hideminor": "Ocultar cambios menores.",
+       "apihelp-feedrecentchanges-param-hidebots": "Ocultar los cambios realizados por bots.",
+       "apihelp-feedrecentchanges-param-hideanons": "Ocultar los cambios realizados por usuarios anónimos.",
+       "apihelp-feedrecentchanges-param-hideliu": "Ocultar los cambios realizados por usuarios registrados.",
+       "apihelp-feedrecentchanges-param-hidepatrolled": "Ocultar los cambios patrullados.",
+       "apihelp-feedrecentchanges-param-hidemyself": "Ocultar los cambios realizados por ti.",
+       "apihelp-feedrecentchanges-param-tagfilter": "Filtrar por etiquetas.",
+       "apihelp-feedrecentchanges-param-target": "Mostrar solo los cambios en las páginas enlazadas en esta.",
+       "apihelp-feedrecentchanges-param-showlinkedto": "Mostrar los cambios en páginas enlazadas con la página seleccionada.",
+       "apihelp-feedrecentchanges-example-simple": "Mostrar los cambios recientes",
+       "apihelp-feedrecentchanges-example-30days": "Mostrar los cambios recientes limitados a 30 días",
+       "apihelp-feedwatchlist-description": "Devuelve el canal de una lista de seguimiento.",
+       "apihelp-feedwatchlist-param-feedformat": "El formato del canal.",
+       "apihelp-filerevert-description": "Revertir el archivo a una versión anterior.",
+       "apihelp-filerevert-param-filename": "Nombre de archivo final, sin el prefijo Archivo:",
+       "apihelp-filerevert-param-comment": "Comentario de carga.",
+       "apihelp-help-example-main": "Ayuda del módulo principal",
+       "apihelp-help-example-recursive": "Toda la ayuda en una página",
+       "apihelp-help-example-help": "Ayuda del módulo de ayuda en sí",
+       "apihelp-imagerotate-description": "Girar una o más imágenes.",
        "apihelp-import-param-summary": "Resumen de importación.",
+       "apihelp-import-param-xml": "Se cargó el archivo XML.",
+       "apihelp-import-param-rootpage": "Importar como subpágina de esta página.",
        "apihelp-login-param-name": "Nombre de usuario.",
        "apihelp-login-param-password": "Contraseña.",
        "apihelp-login-param-domain": "Dominio (opcional).",
+       "apihelp-login-example-login": "Acceder",
+       "apihelp-logout-description": "Salir y vaciar los datos de la sesión.",
+       "apihelp-logout-example-logout": "Cerrar la sesión del usuario actual",
        "apihelp-move-description": "Mover una página.",
+       "apihelp-move-param-reason": "Motivo del traslado.",
+       "apihelp-move-param-movetalk": "Trasladar la página de discusión si existe.",
+       "apihelp-move-param-movesubpages": "Trasladar las subpáginas si procede.",
+       "apihelp-move-param-noredirect": "No crear una redirección.",
+       "apihelp-move-param-watch": "Añadir la página y su redirección a tu lista de seguimiento.",
+       "apihelp-move-param-unwatch": "Quitar la página y su redirección de tu lista de seguimiento.",
+       "apihelp-move-param-ignorewarnings": "Ignorar cualquier aviso.",
+       "apihelp-opensearch-description": "Buscar en el wiki mediante el protocolo OpenSearch.",
        "apihelp-opensearch-param-search": "Buscar cadena.",
        "apihelp-options-example-reset": "Restablecer todas las preferencias",
        "apihelp-patrol-example-rcid": "Patrullar un cambio reciente",
        "apihelp-patrol-example-revid": "Patrullar una revisión",
+       "apihelp-protect-param-reason": "Motivo de la (des)protección.",
        "apihelp-protect-example-protect": "Proteger una página",
+       "apihelp-query+allimages-param-sha1": "Suma SHA1 de la imagen. Invalida $1sha1base36.",
+       "apihelp-query+allimages-param-sha1base36": "Suma SHA1 de la imagen en base 36 (usada en MediaWiki).",
        "apihelp-query+allusers-param-activeusers": "Solo listar usuarios activos en {{PLURAL:$1|el último día|los $1 últimos días}}.",
        "apihelp-query+images-description": "Devuelve todos los archivos contenidos en las páginas dadas.",
        "apihelp-query+search-param-info": "Qué metadatos devolver.",
index 822c964..9c377df 100644 (file)
@@ -58,12 +58,6 @@ class LinksUpdate extends SqlDataUpdate {
        /** @var array Map of arbitrary name to value */
        public $mProperties;
 
-       /** @var DatabaseBase Database connection reference */
-       public $mDb;
-
-       /** @var array SELECT options to be used */
-       public $mOptions;
-
        /** @var bool Whether to queue jobs for recursive updates */
        public $mRecursive;
 
index 7ec61ea..5823b2e 100644 (file)
@@ -31,7 +31,7 @@
  *       the beginTransaction() and commitTransaction() methods.
  */
 abstract class SqlDataUpdate extends DataUpdate {
-       /** @var DatabaseBase Database connection reference */
+       /** @var IDatabase Database connection reference */
        protected $mDb;
 
        /** @var array SELECT options to be used (array) */
@@ -53,9 +53,7 @@ abstract class SqlDataUpdate extends DataUpdate {
        public function __construct( $withTransaction = true ) {
                parent::__construct();
 
-               // @todo Get connection only when it's needed? Make sure that doesn't
-               // break anything, especially transactions!
-               $this->mDb = wfGetDB( DB_MASTER );
+               $this->mDb = wfGetLB()->getLazyConnectionRef( DB_MASTER );
 
                $this->mWithTransaction = $withTransaction;
                $this->mHasTransaction = false;
index 5f70362..e54f748 100644 (file)
@@ -20,28 +20,19 @@ class HTMLCheckField extends HTMLFormField {
                        $attr['class'] = $this->mClass;
                }
 
-               if ( $this->mParent->isVForm() ) {
-                       // Nest checkbox inside label.
-                       return Html::rawElement( 'label',
-                               array(
-                                       'class' => 'mw-ui-checkbox-label'
-                               ),
-                               Xml::check( $this->mName, $value, $attr ) . $this->mLabel );
-               } else {
-                       $chkLabel = Xml::check( $this->mName, $value, $attr )
-                       . '&#160;'
-                       . Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
-
-                       if ( $wgUseMediaWikiUIEverywhere ) {
-                               $chkLabel = Html::rawElement(
-                                       'div',
-                                       array( 'class' => 'mw-ui-checkbox' ),
-                                       $chkLabel
-                               );
-                       }
+               $chkLabel = Xml::check( $this->mName, $value, $attr )
+               . '&#160;'
+               . Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
 
-                       return $chkLabel;
+               if ( $wgUseMediaWikiUIEverywhere || $this->mParent instanceof VFormHTMLForm ) {
+                       $chkLabel = Html::rawElement(
+                               'div',
+                               array( 'class' => 'mw-ui-checkbox' ),
+                               $chkLabel
+                       );
                }
+
+               return $chkLabel;
        }
 
        /**
index dc73522..908fdf2 100644 (file)
@@ -207,9 +207,40 @@ class HTMLForm extends ContextSource {
                'table',
                'div',
                'raw',
+       );
+
+       /**
+        * Available formats in which to display the form
+        * @var array
+        */
+       protected $availableSubclassDisplayFormats = array(
                'vform',
        );
 
+       /**
+        * Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
+        *
+        * @throws MWException When the display format requested is not known
+        * @param string $displayFormat
+        * @param mixed $arguments... Additional arguments to pass to the constructor.
+        * @return HTMLForm
+        */
+       public static function factory( $displayFormat/*, $arguments...*/ ) {
+               $arguments = func_get_args();
+               array_shift( $arguments );
+
+               switch ( $displayFormat ) {
+                       case 'vform':
+                               $reflector = new ReflectionClass( 'VFormHTMLForm' );
+                               return $reflector->newInstanceArgs( $arguments );
+                       default:
+                               $reflector = new ReflectionClass( 'HTMLForm' );
+                               $form = $reflector->newInstanceArgs( $arguments );
+                               $form->setDisplayFormat( $displayFormat );
+                               return $form;
+               }
+       }
+
        /**
         * Build a new HTMLForm from an array of field attributes
         *
@@ -233,6 +264,11 @@ class HTMLForm extends ContextSource {
                        $this->mMessagePrefix = $context;
                }
 
+               // Evil hack for mobile :(
+               if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $this->displayFormat === 'table' ) {
+                       $this->displayFormat = 'div';
+               }
+
                // Expand out into a tree.
                $loadedDescriptor = array();
                $this->mFlatFields = array();
@@ -246,12 +282,7 @@ class HTMLForm extends ContextSource {
                                $this->mUseMultipart = true;
                        }
 
-                       $field = self::loadInputFromParameters( $fieldname, $info, $this );
-
-                       // vform gets too much space if empty labels generate HTML.
-                       if ( $this->isVForm() ) {
-                               $field->setShowEmptyLabel( false );
-                       }
+                       $field = static::loadInputFromParameters( $fieldname, $info, $this );
 
                        $setSection =& $loadedDescriptor;
                        if ( $section ) {
@@ -286,10 +317,24 @@ class HTMLForm extends ContextSource {
         * @return HTMLForm $this for chaining calls (since 1.20)
         */
        public function setDisplayFormat( $format ) {
+               if (
+                       in_array( $format, $this->availableSubclassDisplayFormats ) ||
+                       in_array( $this->displayFormat, $this->availableSubclassDisplayFormats )
+               ) {
+                       throw new MWException( 'Cannot change display format after creation, ' .
+                               'use HTMLForm::factory() instead' );
+               }
+
                if ( !in_array( $format, $this->availableDisplayFormats ) ) {
                        throw new MWException( 'Display format must be one of ' .
                                print_r( $this->availableDisplayFormats, true ) );
                }
+
+               // Evil hack for mobile :(
+               if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
+                       $format = 'div';
+               }
+
                $this->displayFormat = $format;
 
                return $this;
@@ -301,20 +346,17 @@ class HTMLForm extends ContextSource {
         * @return string
         */
        public function getDisplayFormat() {
-               $format = $this->displayFormat;
-               if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
-                       $format = 'div';
-               }
-               return $format;
+               return $this->displayFormat;
        }
 
        /**
         * Test if displayFormat is 'vform'
         * @since 1.22
+        * @deprecated since 1.25
         * @return bool
         */
        public function isVForm() {
-               return $this->displayFormat === 'vform';
+               return false;
        }
 
        /**
@@ -337,7 +379,7 @@ class HTMLForm extends ContextSource {
                if ( isset( $descriptor['class'] ) ) {
                        $class = $descriptor['class'];
                } elseif ( isset( $descriptor['type'] ) ) {
-                       $class = self::$typeMappings[$descriptor['type']];
+                       $class = static::$typeMappings[$descriptor['type']];
                        $descriptor['class'] = $class;
                } else {
                        $class = null;
@@ -362,7 +404,7 @@ class HTMLForm extends ContextSource {
         * @return HTMLFormField Instance of a subclass of HTMLFormField
         */
        public static function loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent = null ) {
-               $class = self::getClassFromDescriptor( $fieldname, $descriptor );
+               $class = static::getClassFromDescriptor( $fieldname, $descriptor );
 
                $descriptor['fieldname'] = $fieldname;
                if ( $parent ) {
@@ -790,19 +832,6 @@ class HTMLForm extends ContextSource {
                # For good measure (it is the default)
                $this->getOutput()->preventClickjacking();
                $this->getOutput()->addModules( 'mediawiki.htmlform' );
-               if ( $this->isVForm() ) {
-                       // This is required for VForm HTMLForms that use that style regardless
-                       // of wgUseMediaWikiUIEverywhere (since they pre-date it).
-                       // When wgUseMediaWikiUIEverywhere is removed, this should be consolidated
-                       // with the addModuleStyles in SpecialPage->setHeaders.
-                       $this->getOutput()->addModuleStyles( array(
-                               'mediawiki.ui',
-                               'mediawiki.ui.button',
-                               'mediawiki.ui.input',
-                       ) );
-                       // @todo Should vertical form set setWrapperLegend( false )
-                       // to hide ugly fieldsets?
-               }
 
                $html = ''
                        . $this->getErrors( $submitResult )
@@ -818,18 +847,10 @@ class HTMLForm extends ContextSource {
        }
 
        /**
-        * Wrap the form innards in an actual "<form>" element
-        *
-        * @param string $html HTML contents to wrap.
-        *
-        * @return string Wrapped HTML.
+        * Get HTML attributes for the `<form>` tag.
+        * @return array
         */
-       function wrapForm( $html ) {
-
-               # Include a <fieldset> wrapper for style, if requested.
-               if ( $this->mWrapperLegend !== false ) {
-                       $html = Xml::fieldset( $this->mWrapperLegend, $html );
-               }
+       protected function getFormAttributes() {
                # Use multipart/form-data
                $encType = $this->mUseMultipart
                        ? 'multipart/form-data'
@@ -844,12 +865,23 @@ class HTMLForm extends ContextSource {
                if ( !empty( $this->mId ) ) {
                        $attribs['id'] = $this->mId;
                }
+               return $attribs;
+       }
 
-               if ( $this->isVForm() ) {
-                       array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' );
+       /**
+        * Wrap the form innards in an actual "<form>" element
+        *
+        * @param string $html HTML contents to wrap.
+        *
+        * @return string Wrapped HTML.
+        */
+       function wrapForm( $html ) {
+               # Include a <fieldset> wrapper for style, if requested.
+               if ( $this->mWrapperLegend !== false ) {
+                       $html = Xml::fieldset( $this->mWrapperLegend, $html );
                }
 
-               return Html::rawElement( 'form', $attribs, $html );
+               return Html::rawElement( 'form', $this->getFormAttributes(), $html );
        }
 
        /**
@@ -905,21 +937,10 @@ class HTMLForm extends ContextSource {
 
                        $attribs['class'] = array( 'mw-htmlform-submit' );
 
-                       if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
+                       if ( $useMediaWikiUIEverywhere ) {
                                array_push( $attribs['class'], 'mw-ui-button', $this->mSubmitModifierClass );
                        }
 
-                       if ( $this->isVForm() ) {
-                               // mw-ui-block is necessary because the buttons aren't necessarily in an
-                               // immediate child div of the vform.
-                               // @todo Let client specify if the primary submit button is progressive or destructive
-                               array_push(
-                                       $attribs['class'],
-                                       'mw-ui-big',
-                                       'mw-ui-block'
-                               );
-                       }
-
                        $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
                }
 
@@ -928,7 +949,8 @@ class HTMLForm extends ContextSource {
                                'input',
                                array(
                                        'type' => 'reset',
-                                       'value' => $this->msg( 'htmlform-reset' )->text()
+                                       'value' => $this->msg( 'htmlform-reset' )->text(),
+                                       'class' => ( $useMediaWikiUIEverywhere ? 'mw-ui-button' : null ),
                                )
                        ) . "\n";
                }
@@ -948,15 +970,9 @@ class HTMLForm extends ContextSource {
                                $attrs['id'] = $button['id'];
                        }
 
-                       if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
-                               if ( isset( $attrs['class'] ) ) {
-                                       $attrs['class'] .= ' mw-ui-button';
-                               } else {
-                                       $attrs['class'] = 'mw-ui-button';
-                               }
-                               if ( $this->isVForm() ) {
-                                       $attrs['class'] .= ' mw-ui-big mw-ui-block';
-                               }
+                       if ( $useMediaWikiUIEverywhere ) {
+                               $attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : array();
+                               $attrs['class'][] = 'mw-ui-button';
                        }
 
                        $buttons .= Html::element( 'input', $attrs ) . "\n";
@@ -965,13 +981,6 @@ class HTMLForm extends ContextSource {
                $html = Html::rawElement( 'span',
                        array( 'class' => 'mw-htmlform-submit-buttons' ), "\n$buttons" ) . "\n";
 
-               // Buttons are top-level form elements in table and div layouts,
-               // but vform wants all elements inside divs to get spaced-out block
-               // styling.
-               if ( $this->mShowSubmit && $this->isVForm() ) {
-                       $html = Html::rawElement( 'div', null, "\n$html" ) . "\n";
-               }
-
                return $html;
        }
 
@@ -1284,20 +1293,8 @@ class HTMLForm extends ContextSource {
                $subsectionHtml = '';
                $hasLabel = false;
 
-               switch ( $displayFormat ) {
-                       case 'table':
-                               $getFieldHtmlMethod = 'getTableRow';
-                               break;
-                       case 'vform':
-                               // Close enough to a div.
-                               $getFieldHtmlMethod = 'getDiv';
-                               break;
-                       case 'div':
-                               $getFieldHtmlMethod = 'getDiv';
-                               break;
-                       default:
-                               $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
-               }
+               // Conveniently, PHP method names are case-insensitive.
+               $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
 
                foreach ( $fields as $key => $value ) {
                        if ( $value instanceof HTMLFormField ) {
@@ -1369,7 +1366,7 @@ class HTMLForm extends ContextSource {
                                $html = Html::rawElement( 'table',
                                                $attribs,
                                                Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
-                       } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
+                       } else {
                                $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
                        }
                }
index a91f331..a751900 100644 (file)
@@ -513,9 +513,6 @@ abstract class HTMLFormField {
                        $inputHtml . "\n$errors"
                );
                $divCssClasses = array( "mw-htmlform-field-$fieldType", $this->mClass, $errorClass );
-               if ( $this->mParent->isVForm() ) {
-                       $divCssClasses[] = 'mw-ui-vform-field';
-               }
 
                $wrapperAttributes = array(
                        'class' => $divCssClasses,
@@ -554,6 +551,20 @@ abstract class HTMLFormField {
                return $html;
        }
 
+       /**
+        * Get the complete field for the input, including help text,
+        * labels, and whatever. Fall back from 'vform' to 'div' when not overridden.
+        *
+        * @since 1.25
+        * @param string $value The value to set the input to.
+        * @return string Complete HTML field.
+        */
+       public function getVForm( $value ) {
+               // Ewwww
+               $this->mClass .= ' mw-ui-vform-field';
+               return $this->getDiv( $value );
+       }
+
        /**
         * Generate help text HTML in table format
         * @since 1.20
index d1b7746..b06f10d 100644 (file)
@@ -262,17 +262,8 @@ class HTMLFormFieldCloner extends HTMLFormField {
                        ? $this->mParams['format']
                        : $this->mParent->getDisplayFormat();
 
-               switch ( $displayFormat ) {
-                       case 'table':
-                               $getFieldHtmlMethod = 'getTableRow';
-                               break;
-                       case 'vform':
-                               // Close enough to a div.
-                               $getFieldHtmlMethod = 'getDiv';
-                               break;
-                       default:
-                               $getFieldHtmlMethod = 'get' . ucfirst( $displayFormat );
-               }
+               // Conveniently, PHP method names are case-insensitive.
+               $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
 
                $html = '';
                $hidden = '';
@@ -336,7 +327,7 @@ class HTMLFormFieldCloner extends HTMLFormField {
                                $html = Html::rawElement( 'table',
                                        $attribs,
                                        Html::rawElement( 'tbody', array(), "\n$html\n" ) ) . "\n";
-                       } elseif ( $displayFormat === 'div' || $displayFormat === 'vform' ) {
+                       } else {
                                $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
                        }
                }
diff --git a/includes/htmlform/VFormHTMLForm.php b/includes/htmlform/VFormHTMLForm.php
new file mode 100644 (file)
index 0000000..7826a0c
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * HTML form generation and submission handling, vertical-form style.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Compact stacked vertical format for forms.
+ */
+class VFormHTMLForm extends HTMLForm {
+       /**
+        * Wrapper and its legend are never generated in VForm mode.
+        * @var boolean
+        */
+       protected $mWrapperLegend = false;
+
+       /**
+        * Symbolic display format name.
+        * @var string
+        */
+       protected $displayFormat = 'vform';
+
+       public function isVForm() {
+               return true;
+       }
+
+       public static function loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent = null ) {
+               $field = parent::loadInputFromParameters( $fieldname, $descriptor, $parent );
+               $field->setShowEmptyLabel( false );
+               return $field;
+       }
+
+       function getHTML( $submitResult ) {
+               // This is required for VForm HTMLForms that use that style regardless
+               // of wgUseMediaWikiUIEverywhere (since they pre-date it).
+               // When wgUseMediaWikiUIEverywhere is removed, this should be consolidated
+               // with the addModuleStyles in SpecialPage->setHeaders.
+               $this->getOutput()->addModuleStyles( array(
+                       'mediawiki.ui',
+                       'mediawiki.ui.button',
+                       'mediawiki.ui.input',
+                       'mediawiki.ui.checkbox',
+               ) );
+
+               return parent::getHTML( $submitResult );
+       }
+
+       protected function getFormAttributes() {
+               $attribs = parent::getFormAttributes();
+               array_push( $attribs['class'], 'mw-ui-vform', 'mw-ui-container' );
+               return $attribs;
+       }
+
+       function wrapForm( $html ) {
+               // Always discard $this->mWrapperLegend
+               return Html::rawElement( 'form', $this->getFormAttributes(), $html );
+       }
+
+       function getButtons() {
+               $buttons = '';
+
+               if ( $this->mShowSubmit ) {
+                       $attribs = array();
+
+                       if ( isset( $this->mSubmitID ) ) {
+                               $attribs['id'] = $this->mSubmitID;
+                       }
+
+                       if ( isset( $this->mSubmitName ) ) {
+                               $attribs['name'] = $this->mSubmitName;
+                       }
+
+                       if ( isset( $this->mSubmitTooltip ) ) {
+                               $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
+                       }
+
+                       $attribs['class'] = array(
+                               'mw-htmlform-submit',
+                               'mw-ui-button mw-ui-big mw-ui-block',
+                               $this->mSubmitModifierClass,
+                       );
+
+                       $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
+               }
+
+               if ( $this->mShowReset ) {
+                       $buttons .= Html::element(
+                               'input',
+                               array(
+                                       'type' => 'reset',
+                                       'value' => $this->msg( 'htmlform-reset' )->text(),
+                                       'class' => 'mw-ui-button mw-ui-big mw-ui-block',
+                               )
+                       ) . "\n";
+               }
+
+               foreach ( $this->mButtons as $button ) {
+                       $attrs = array(
+                               'type' => 'submit',
+                               'name' => $button['name'],
+                               'value' => $button['value']
+                       );
+
+                       if ( $button['attribs'] ) {
+                               $attrs += $button['attribs'];
+                       }
+
+                       if ( isset( $button['id'] ) ) {
+                               $attrs['id'] = $button['id'];
+                       }
+
+                       $attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : array();
+                       $attrs['class'][] = 'mw-ui-button mw-ui-big mw-ui-block';
+
+                       $buttons .= Html::element( 'input', $attrs ) . "\n";
+               }
+
+               $html = Html::rawElement( 'div',
+                       array( 'class' => 'mw-htmlform-submit-buttons' ), "\n$buttons" ) . "\n";
+
+               return $html;
+       }
+}
index 4159c1d..dc52554 100644 (file)
@@ -727,7 +727,7 @@ abstract class Installer {
                }
                $databases = array_flip( $databases );
                if ( !$databases ) {
-                       $this->showError( 'config-no-db', $wgLang->commaList( $allNames ) );
+                       $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
 
                        // @todo FIXME: This only works for the web installer!
                        return false;
@@ -1469,15 +1469,16 @@ abstract class Installer {
        }
 
        /**
-        * Returns a default value to be used for $wgDefaultSkin: the preferred skin, if available among
-        * the installed skins, or any other one otherwise.
+        * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
+        * but will fall back to another if the default skin is missing and some other one is present
+        * instead.
         *
         * @param string[] $skinNames Names of installed skins.
         * @return string
         */
        public function getDefaultSkin( array $skinNames ) {
                $defaultSkin = $GLOBALS['wgDefaultSkin'];
-               if ( in_array( $defaultSkin, $skinNames ) ) {
+               if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
                        return $defaultSkin;
                } else {
                        return $skinNames[0];
index 9ecb24b..038c2be 100644 (file)
@@ -1033,14 +1033,15 @@ class WebInstallerOptions extends WebInstallerPage {
                $skins = $this->parent->findExtensions( 'skins' );
                $skinHtml = $this->getFieldSetStart( 'config-skins' );
 
-               if ( $skins ) {
-                       $skinNames = array_map( 'strtolower', $skins );
+               $skinNames = array_map( 'strtolower', $skins );
+               $chosenSkinName = $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) );
 
+               if ( $skins ) {
                        $radioButtons = $this->parent->getRadioElements( array(
                                'var' => 'wgDefaultSkin',
                                'itemLabels' => array_fill_keys( $skinNames, 'config-skins-use-as-default' ),
                                'values' => $skinNames,
-                               'value' => $this->getVar( 'wgDefaultSkin', $this->parent->getDefaultSkin( $skinNames ) ),
+                               'value' => $chosenSkinName,
                        ) );
 
                        foreach ( $skins as $skin ) {
@@ -1055,7 +1056,9 @@ class WebInstallerOptions extends WebInstallerPage {
                                        '</div>';
                        }
                } else {
-                       $skinHtml .= $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() );
+                       $skinHtml .=
+                               $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() ) .
+                               Html::hidden( 'config_wgDefaultSkin', $chosenSkinName );
                }
 
                $skinHtml .= $this->parent->getHelpBox( 'config-skins-help' ) .
index 4857495..dfb6344 100644 (file)
@@ -49,7 +49,7 @@
        "config-unicode-using-intl": "Using the [http://pecl.php.net/intl intl PECL extension] for Unicode normalization.",
        "config-unicode-pure-php-warning": "<strong>Warning:</strong> The [http://pecl.php.net/intl intl PECL extension] is not available to handle Unicode normalization, falling back to slow pure-PHP implementation.\nIf you run a high-traffic site, you should read a little on [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations Unicode normalization].",
        "config-unicode-update-warning": "<strong>Warning:</strong> The installed version of the Unicode normalization wrapper uses an older version of [http://site.icu-project.org/ the ICU project's] library.\nYou should [//www.mediawiki.org/wiki/Special:MyLanguage/Unicode_normalization_considerations upgrade] if you are at all concerned about using Unicode.",
-       "config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database types are supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.",
+       "config-no-db": "Could not find a suitable database driver! You need to install a database driver for PHP.\nThe following database {{PLURAL:$2|type is|types are}} supported: $1.\n\nIf you compiled PHP yourself, reconfigure it with a database client enabled, for example, using <code>./configure --with-mysqli</code>.\nIf you installed PHP from a Debian or Ubuntu package, then you also need to install, for example, the <code>php5-mysql</code> package.",
        "config-outdated-sqlite": "<strong>Warning:</strong> you have SQLite $1, which is lower than minimum required version $2. SQLite will be unavailable.",
        "config-no-fts3": "<strong>Warning:</strong> SQLite is compiled without the [//sqlite.org/fts3.html FTS3 module], search features will be unavailable on this backend.",
        "config-register-globals-error": "<strong>Error: PHP's <code>[http://php.net/register_globals register_globals]</code> option is enabled.\nIt must be disabled to continue with the installation.</strong>\nSee [https://www.mediawiki.org/wiki/register_globals https://www.mediawiki.org/wiki/register_globals] for help on how to do so.",
index 772ce96..3a9f267 100644 (file)
@@ -67,7 +67,7 @@
        "config-unicode-using-intl": "Status message in the MediaWiki installer environment checks.",
        "config-unicode-pure-php-warning": "PECL is the name of a group producing standard pieces of software for PHP, and intl is the name of their library handling some aspects of internationalization.",
        "config-unicode-update-warning": "ICU is a body producing standard software tools for support of Unicode and other internationalization aspects. This message warns the system administrator installing MediaWiki that the server's software is not up-to-date and MediaWiki will have problems handling some characters.",
-       "config-no-db": "{{doc-important|Do not translate \"<code>./configure --with-mysqli</code>\" and \"<code>php5-mysql</code>\".}}\nParameters:\n* $1 is comma separated list of database types supported by MediaWiki.",
+       "config-no-db": "{{doc-important|Do not translate \"<code>./configure --with-mysqli</code>\" and \"<code>php5-mysql</code>\".}}\nParameters:\n* $1 is comma separated list of database types supported by MediaWiki.\n* $2 is the count of items in $1 - for use in plural.",
        "config-outdated-sqlite": "Used as warning. Parameters:\n* $1 - the version of SQLite that has been installed\n* $2 - minimum version",
        "config-no-fts3": "A \"[[:wikipedia:Front and back ends|backend]]\" is a system or component that ordinary users don't interact with directly and don't need to know about, and that is responsible for a distinct task or service - for example, a storage back-end is a generic system for storing data which other applications can use. Possible alternatives for back-end are \"system\" or \"service\", or (depending on context and language) even leave it untranslated.",
        "config-register-globals-error": "Error message in the MediaWiki installer environment checks.",
index b4409c3..b8f67c2 100644 (file)
@@ -241,12 +241,13 @@ class ImagePage extends Article {
                        '<li><a href="#filehistory">' . $this->getContext()->msg( 'filehist' )->escaped() . '</a></li>',
                        '<li><a href="#filelinks">' . $this->getContext()->msg( 'imagelinks' )->escaped() . '</a></li>',
                );
+
+               Hooks::run( 'ImagePageShowTOC', array( $this, &$r ) );
+
                if ( $metadata ) {
                        $r[] = '<li><a href="#metadata">' . $this->getContext()->msg( 'metadata' )->escaped() . '</a></li>';
                }
 
-               Hooks::run( 'ImagePageShowTOC', array( $this, &$r ) );
-
                return '<ul id="filetoc">' . implode( "\n", $r ) . '</ul>';
        }
 
index fd3bc7b..f727c05 100644 (file)
@@ -74,6 +74,16 @@ abstract class FormSpecialPage extends SpecialPage {
                return strtolower( $this->getName() );
        }
 
+       /**
+        * Get display format for the form. See HTMLForm documentation for available values.
+        *
+        * @since 1.25
+        * @return string
+        */
+       protected function getDisplayFormat() {
+               return 'table';
+       }
+
        /**
         * Get the HTMLForm to control behavior
         * @return HTMLForm|null
@@ -81,15 +91,9 @@ abstract class FormSpecialPage extends SpecialPage {
        protected function getForm() {
                $this->fields = $this->getFormFields();
 
-               $form = new HTMLForm( $this->fields, $this->getContext(), $this->getMessagePrefix() );
+               $form = HTMLForm::factory( $this->getDisplayFormat(), $this->fields, $this->getContext(), $this->getMessagePrefix() );
                $form->setSubmitCallback( array( $this, 'onSubmit' ) );
-               // If the form is a compact vertical form, then don't output this ugly
-               // fieldset surrounding it.
-               // XXX Special pages can setDisplayFormat to 'vform' in alterForm(), but that
-               // is called after this.
-               if ( !$form->isVForm() ) {
-                       $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
-               }
+               $form->setWrapperLegendMsg( $this->getMessagePrefix() . '-legend' );
 
                $headerMsg = $this->msg( $this->getMessagePrefix() . '-text' );
                if ( !$headerMsg->isDisabled() ) {
index 06ede61..674cbc8 100644 (file)
@@ -106,11 +106,13 @@ class SpecialChangeEmail extends FormSpecialPage {
                return $fields;
        }
 
+       protected function getDisplayFormat() {
+               return 'vform';
+       }
+
        protected function alterForm( HTMLForm $form ) {
-               $form->setDisplayFormat( 'vform' );
                $form->setId( 'mw-changeemail-form' );
                $form->setTableId( 'mw-changeemail-table' );
-               $form->setWrapperLegend( false );
                $form->setSubmitTextMsg( 'changeemail-submit' );
                $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
        }
index fa719eb..492105d 100644 (file)
@@ -192,7 +192,6 @@ HTML;
         */
        private function exportQUnit() {
                $out = $this->getOutput();
-
                $out->disable();
 
                $rl = $out->getResourceLoader();
index 52c2460..79b2444 100644 (file)
@@ -90,9 +90,11 @@ class SpecialPageLanguage extends FormSpecialPage {
                return $this->showLogFragment( $this->par );
        }
 
+       protected function getDisplayFormat() {
+               return 'vform';
+       }
+
        public function alterForm( HTMLForm $form ) {
-               $form->setDisplayFormat( 'vform' );
-               $form->setWrapperLegend( false );
                Hooks::run( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) );
        }
 
index 6ee3290..a2dc2ad 100644 (file)
@@ -103,16 +103,13 @@ class SpecialPasswordReset extends FormSpecialPage {
                return $a;
        }
 
+       protected function getDisplayFormat() {
+               return 'vform';
+       }
+
        public function alterForm( HTMLForm $form ) {
                $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
 
-               $form->setDisplayFormat( 'vform' );
-               // Turn the old-school line around the form off.
-               // XXX This wouldn't be necessary here if we could set the format of
-               // the HTMLForm to 'vform' at its creation, but there's no way to do so
-               // from a FormSpecialPage class.
-               $form->setWrapperLegend( false );
-
                $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) );
 
                $i = 0;
index 8dc9bb6..1b85ff8 100644 (file)
@@ -461,7 +461,7 @@ class SpecialUpload extends SpecialPage {
                // Get the page text if this is not a reupload
                if ( !$this->mForReUpload ) {
                        $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
-                               $this->mCopyrightStatus, $this->mCopyrightSource );
+                               $this->mCopyrightStatus, $this->mCopyrightSource, $this->getConfig() );
                } else {
                        $pageText = false;
                }
@@ -491,28 +491,32 @@ class SpecialUpload extends SpecialPage {
         * @param string $license
         * @param string $copyStatus
         * @param string $source
+        * @param Config $config Configuration object to load data from
         * @return string
-        * @todo Use Config obj instead of globals
         */
        public static function getInitialPageText( $comment = '', $license = '',
-               $copyStatus = '', $source = ''
+               $copyStatus = '', $source = '', Config $config = null
        ) {
-               global $wgUseCopyrightUpload, $wgForceUIMsgAsContentMsg;
+               if ( $config === null ) {
+                       wfDebug( __METHOD__ . ' called without a Config instance passed to it' );
+                       $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+               }
 
                $msg = array();
+               $forceUIMsgAsContentMsg = (array)$config->get( 'ForceUIMsgAsContentMsg' );
                /* These messages are transcluded into the actual text of the description page.
                 * Thus, forcing them as content messages makes the upload to produce an int: template
                 * instead of hardcoding it there in the uploader language.
                 */
                foreach ( array( 'license-header', 'filedesc', 'filestatus', 'filesource' ) as $msgName ) {
-                       if ( in_array( $msgName, (array)$wgForceUIMsgAsContentMsg ) ) {
+                       if ( in_array( $msgName, $forceUIMsgAsContentMsg ) ) {
                                $msg[$msgName] = "{{int:$msgName}}";
                        } else {
                                $msg[$msgName] = wfMessage( $msgName )->inContentLanguage()->text();
                        }
                }
 
-               if ( $wgUseCopyrightUpload ) {
+               if ( $config->get( 'UseCopyrightUpload' ) ) {
                        $licensetxt = '';
                        if ( $license != '' ) {
                                $licensetxt = '== ' . $msg['license-header'] . " ==\n" . '{{' . $license . '}}' . "\n";
index 783b547..9f4152a 100644 (file)
                ]
        },
        "tog-underline": "Keçidlərin altını xətlə:",
-       "tog-hideminor": "Son dəyişikliklər kiçik redaktələri gizlə",
-       "tog-hidepatrolled": "Yoxlanılmış redaktələri son dəyişikliklərdə göstərmə",
-       "tog-newpageshidepatrolled": "Yoxlanılmış səhifələri yeni səhifə siyahısında göstərmə",
-       "tog-extendwatchlist": "Təkmil izləmə siyahısı",
-       "tog-usenewrc": "Son dəyişikliklərin təkmil versiyası (JavaScript)",
-       "tog-numberheadings": "Başlıqların avto-nömrələnməsi",
-       "tog-showtoolbar": "Redaktə zamanı alətlər qutusunu göstər (JavaScript)",
-       "tog-editondblclick": "Səhifələri iki kliklə redaktə etməyə başla (JavaScript)",
-       "tog-editsectiononrightclick": "Bölmələrin redaktəsini başlıqların üzərində sağ klik etməklə mümkün et (JavaScript)",
-       "tog-watchcreations": "Yaratdığım səhifələri izlədiyim səhifələrə əlavə et",
-       "tog-watchdefault": "Redaktə etdiyim səhifələri izlədiyim səhifələrə əlavə et",
-       "tog-watchmoves": "Adlarını dəyişdiyim səhifələri izlədiyim səhifələrə əlavə et",
-       "tog-watchdeletion": "Sildiyim səhifələri izlədiyim səhifələrə əlavə et",
-       "tog-minordefault": "Default olaraq bütün redaktələri kiçik redaktə kimi nişanla",
+       "tog-hideminor": "Son dəyişikliklər siyahısında kiçik redaktələri gizlə",
+       "tog-hidepatrolled": "Son dəyişikliklər siyahısında yoxlanılmış redaktələri gizlə",
+       "tog-newpageshidepatrolled": "Yeni səhifələr siyahısında yoxlanılmış səhifələri gizlə",
+       "tog-extendwatchlist": "Yalnız son dəyişiklikləri yox, bütün dəyişiklikləri göstərmək üçün izləmə siyahısını genişlət",
+       "tog-usenewrc": "Son dəyişikliklərdəki və izləmə siyahısındakı dəyişiklikləri qruplaşdır",
+       "tog-numberheadings": "Başlıqları avtomatik nömrələ",
+       "tog-showtoolbar": "Redaktə zamanı üstdəki alətlər qutusunu göstər",
+       "tog-editondblclick": "Səhifələri iki kliklə redaktə et",
+       "tog-editsectiononrightclick": "Bölmə başlığı üzərində siçanın sağ düyməsini klikləməklə bölmələri redaktə et",
+       "tog-watchcreations": "Yaratdığım səhifələri və yüklədiyim faylları izlədiyim səhifələrə əlavə et",
+       "tog-watchdefault": "Redaktə etdiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
+       "tog-watchmoves": "Adlarını dəyişdiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
+       "tog-watchdeletion": "Sildiyim səhifələri və faylları izlədiyim səhifələrə əlavə et",
+       "tog-minordefault": "Standart olaraq bütün redaktələri kiçik redaktə kimi nişanla",
        "tog-previewontop": "Sınaq göstərişi yazma sahəsindən əvvəl göstər",
        "tog-previewonfirst": "İlkin redaktədə sınaq göstərişi",
        "tog-enotifwatchlistpages": "İzləmə siyahısında olan məqalə redaktə olunsa, mənə e-məktub göndər",
index 8601310..3c787ed 100644 (file)
        "titleprotected": "Стварэньне старонкі з такой назвай было забароненае {{GENDER:$1|ўдзельнікам|ўдзельніцай}} [[User:$1|$1]].\nПрычына забароны: «<em>$2</em>».",
        "filereadonlyerror": "Немагчыма зьмяніць файл «$1», бо файлавае сховішча «$2» знаходзіцца ў рэжыме толькі для чытаньня.\n\nАдміністратар, які абмежаваў доступ, пазначыў прычыну: «$3».",
        "invalidtitle-knownnamespace": "Няслушны загаловак з прасторай назваў «$2» і тэкстам «$3»",
-       "invalidtitle-unknownnamespace": "Няслушная назва ў невядомай прасторы $1: «$2»",
+       "invalidtitle-unknownnamespace": "Няслушны загаловак зь невядомым нумарам прасторы назваў $1 і тэкстам «$2»",
        "exception-nologin": "Вы не ўвайшлі ў сыстэму",
        "exception-nologin-text": "Неабходна ўвайсьці, каб атрымаць доступ да гэтай старонкі або дзеяньня.",
        "exception-nologin-text-manual": "Неабходна $1, каб мець доступ да гэтай старонкі або дзеяньня.",
index 554768d..e969701 100644 (file)
        "mostrevisions": "Страници с най-много версии",
        "prefixindex": "Всички страници с представка",
        "prefixindex-namespace": "Всички страници с представка (именно пространство $1)",
+       "prefixindex-strip": "Скриване на представката в списъка с резултати",
        "shortpages": "Кратки страници",
        "longpages": "Дълги страници",
        "deadendpages": "Задънени страници",
index 1375c84..9f2a58f 100644 (file)
        "api-error-stashfilestorage": "S'ha produït un error en emmagatzemar el fitxer en l'espai temporal.",
        "api-error-stashzerolength": "El servidor no ha pogut desar el fitxer a l'espai temporal perquè tenia longitud zero.",
        "api-error-stashnotloggedin": "Cal haver iniciat una sessió per desar fitxers en l'espai temporal de càrrega.",
-       "api-error-stashwrongowner": "El fitxer que provàveu d'accedir en l'espai de temporal no us pertany.",
+       "api-error-stashwrongowner": "El fitxer que provàveu d'accedir en l'espai temporal no us pertany.",
        "api-error-stashnosuchfilekey": "La clau de fitxer que provàveu d'accedir en l'espai temporal no existeix.",
        "api-error-timeout": "El servidor no ha respost en el temps esperat.",
        "api-error-unclassified": "S'ha produït un error desconegut",
index ebecd5a..b528f1e 100644 (file)
        "uploaderror": "Fehler beim Hochladen",
        "upload-recreate-warning": "'''Achtung: Eine Datei dieses Namens wurde bereits gelöscht oder verschoben.'''\n\nEs folgt ein Auszug aus dem Lösch- und Verschiebungs-Logbuch dieser Datei.",
        "uploadtext": "Benutze dieses Formular, um neue Dateien hochzuladen.\n\nGehe zu der [[Special:FileList|Liste hochgeladener Dateien]], um vorhandene Dateien zu suchen und anzuzeigen. Siehe auch das [[Special:Log/upload|Datei-]] und [[Special:Log/delete|Lösch-Logbuch]].\n\nUm ein '''Bild''' in einer Seite zu verwenden, nutze einen Link in der folgenden Form:\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Datei.jpg]]</nowiki></code>''' – für ein Vollbild\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:Datei.png|200px|thumb|left|Alternativer Text]]</nowiki></code>''' – für ein 200px breites Bild innerhalb einer Box, mit „Alternativer Text“ als Bildbeschreibung\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:Datei.ogg]]</nowiki></code>''' – für einen direkten Link auf die Datei, ohne Darstellung der Datei",
-       "upload-permitted": "Erlaubte Dateitypen: $1.",
-       "upload-preferred": "Bevorzugte Dateitypen: $1.",
-       "upload-prohibited": "Nicht erlaubte Dateitypen: $1.",
+       "upload-permitted": "{{PLURAL:$2|Erlaubter Dateityp|Erlaubte Dateitypen}}: $1.",
+       "upload-preferred": "{{PLURAL:$2|Bevorzugter Dateityp|Bevorzugte Dateitypen}}: $1.",
+       "upload-prohibited": "{{PLURAL:$2|Nicht erlaubter Dateityp|Nicht erlaubte Dateitypen}}: $1.",
        "uploadlogpage": "Datei-Logbuch",
        "uploadlogpagetext": "Dies ist das Logbuch der hochgeladenen Dateien, siehe auch die [[Special:NewFiles|Galerie neuer Dateien]] für einen visuellen Überblick.",
        "filename": "Dateiname",
index 86e6c5f..2b73de5 100644 (file)
        "content-model-javascript": "JavaScript",
        "content-model-css": "CSS",
        "content-json-empty-object": "Objeto vacío",
+       "content-json-empty-array": "Matriz vacía",
        "duplicate-args-category": "Páginas que usan argumentos duplicados en invocaciones de plantillas",
        "duplicate-args-category-desc": "La página contiene invocaciones de plantillas que utilizan argumentos duplicados, como <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> o <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "expensive-parserfunction-warning": "Aviso: Esta página contiene demasiadas llamadas a funciones sintácticas costosas (#ifexist: y similares)\n\nTiene {{PLURAL:$1|una llamada|$1 llamadas}}, pero debería tener menos de $2.",
        "deleteprotected": "No puedes eliminar esta página porque ha sido protegida.",
        "deleting-backlinks-warning": "'''Advertencia:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|Otras páginas]] enlazan o transcluyen la página que vas a eliminar.",
        "rollback": "Revertir ediciones",
-       "rollback_short": "Revertir",
        "rollbacklink": "revertir",
        "rollbacklinkcount": "revertir $1 {{PLURAL:$1|edición|ediciones}}",
        "rollbacklinkcount-morethan": "revertir más de $1 {{PLURAL:$1|edición|ediciones}}",
        "namespace": "Espacio de nombres:",
        "invert": "Invertir selección",
        "tooltip-invert": "Marca esta casilla para ocultar los cambios a las páginas dentro del espacio de nombres seleccionado (y el espacio de nombres asociado si está activada)",
+       "tooltip-whatlinkshere-invert": "Activa esta casilla para ocultar los enlaces dentro del espacio de nombres seleccionado.",
        "namespace_association": "Espacio de nombres asociado",
        "tooltip-namespace_association": "Marca esta casilla para incluir también el espacio de nombres de discusión asociado con el espacio de nombres seleccionado",
        "blanknamespace": "(Principal)",
        "thumbnail-temp-create": "No se ha podido crear el archivo temporal de la miniatura",
        "thumbnail-dest-create": "No se ha podido guardar la miniatura",
        "thumbnail_invalid_params": "Parámetros del thumbnail no válidos",
+       "thumbnail_toobigimagearea": "Archivo más grande que $1",
        "thumbnail_dest_directory": "Incapaz de crear el directorio de destino",
        "thumbnail_image-type": "Tipo de imagen no contemplado",
        "thumbnail_gd-library": "Configuración de la librería GD incompleta: falta la función $1",
        "javascripttest": "Pruebas de JavaScript",
        "javascripttest-pagetext-noframework": "Esta página está reservada para ejecutar pruebas de JavaScript.",
        "javascripttest-pagetext-unknownframework": "Marco de pruebas desconocido \"$1\".",
+       "javascripttest-pagetext-unknownaction": "La acción «$1» es desconocida.",
        "javascripttest-pagetext-frameworks": "Por favor, seleccione uno de los marcos de pruebas siguientes: $1",
        "javascripttest-pagetext-skins": "Elija un aspecto (skin) para ejecutar las pruebas:",
        "javascripttest-qunit-intro": "Consulte la [$1 documentación sobre las pruebas] en mediawiki.org.",
        "version-entrypoints-header-url": "Dirección URL",
        "version-entrypoints-articlepath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgArticlePath Ruta del artículo]",
        "version-entrypoints-scriptpath": "[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgScriptPath Ruta de la secuencia de comandos (script)]",
+       "version-libraries": "Bibliotecas instaladas",
+       "version-libraries-library": "Biblioteca",
+       "version-libraries-version": "Versión",
        "redirect": "Redirigir por archivo, usuario, página o ID de revisión",
        "redirect-legend": "Redirigir a un archivo o página",
        "redirect-summary": "Esta página especial redirige a un fichero (dado un nombre de fichero), a una página (dado un identificador de revisión o de página) o a una página de usuario (dado un identificador numérico de usuario). Uso: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], o [[{{#Special:Redirect}}/user/101]].",
        "compare-revision-not-exists": "La revisión especificada no existe.",
        "dberr-problems": "Lo sentimos. Este sitio está experimentando dificultades técnicas.",
        "dberr-again": "Prueba a recargar dentro de unos minutos.",
-       "dberr-info": "(No se puede contactar con la base de datos del servidor: $1)",
-       "dberr-info-hidden": "(No se puede contactar con la base de datos del servidor)",
+       "dberr-info": "(No se puede acceder a la base de datos: $1)",
+       "dberr-info-hidden": "(No se puede acceder a la base de datos)",
        "dberr-usegoogle": "Mientras tanto puedes probar buscando a través de Google.",
        "dberr-outofdate": "Ten en cuenta que su índice de nuestro contenido puede estar desactualizado.",
        "dberr-cachederror": "La siguiente es una página guardada de la página solicitada, y puede no estar actualizada.",
        "revdelete-uname-unhid": "nombre de usuario mostrado",
        "revdelete-restricted": "restricciones para administradores aplicadas",
        "revdelete-unrestricted": "restricciones para administradores eliminadas",
+       "logentry-merge-merge": "$1 {{GENDER:$2|combinó}} $3 en $4 (revisiones hasta el $5)",
        "logentry-move-move": "$1 movió la página $3 a $4",
        "logentry-move-move-noredirect": "$1 movió la página $3 a $4 sin dejar una redirección",
        "logentry-move-move_redir": "$1 {{GENDER:$2|trasladó}} la página $3 a $4 sobre una redirección",
        "api-error-stashfailed": "Error interno: El servidor no pudo almacenar el archivo temporal.",
        "api-error-publishfailed": "Error interno: el servidor no pudo publicar el archivo temporal.",
        "api-error-stasherror": "Ha ocurrido un error al subir el archivo al depósito.",
+       "api-error-stashedfilenotfound": "No se encontró el archivo del espacio temporal al intentar cargarlo.",
+       "api-error-stashpathinvalid": "La ruta donde debería encontrarse el archivo del espacio temporal no es válida.",
+       "api-error-stashfilestorage": "Ocurrió un error al almacenar el archivo en el espacio temporal.",
+       "api-error-stashzerolength": "El servidor no pudo almacenar el archivo en el espacio temporal porque este no contiene datos.",
+       "api-error-stashnotloggedin": "Debes acceder para guardar archivos en el espacio temporal de carga.",
+       "api-error-stashwrongowner": "El archivo del espacio temporal al que quieres acceder no te pertenece.",
+       "api-error-stashnosuchfilekey": "La clave de archivo del espacio temporal al que quieres acceder no existe.",
        "api-error-timeout": "El servidor no respondió en el plazo previsto.",
        "api-error-unclassified": "Ocurrió un error desconocido.",
        "api-error-unknown-code": "Error desconocido: «$1»",
        "mediastatistics-header-text": "Textual",
        "mediastatistics-header-executable": "Ejecutables",
        "mediastatistics-header-archive": "Formatos comprimidos",
+       "json-warn-trailing-comma": "Se {{PLURAL:$1|eliminó una coma|eliminaron $1 comas}} al final en el archivo JSON",
        "json-error-unknown": "Ocurrió un problema con el código JSON. Error: $1",
+       "json-error-depth": "Se ha superado la profundidad máxima de la pila",
        "json-error-state-mismatch": "JSON no válido o con formato incorrecto",
        "json-error-ctrl-char": "Error de carácter de control, posiblemente codificada incorrectamente",
        "json-error-syntax": "Error de sintaxis",
        "json-error-utf8": "Los caracteres UTF-8 tienen errores de formato; probablemente la codificación es incorrecta.",
+       "json-error-recursion": "Una o más referencias recursivas en el valor por codificar",
        "json-error-inf-or-nan": "Hay uno o más valores «NAN» o «INF» en el valor que se codificará",
        "json-error-unsupported-type": "Se proporcionó un valor en un tipo que no se puede codificar"
 }
index 86724b0..4e38d66 100644 (file)
        "namespace": "Espace de noms :",
        "invert": "Inverser la sélection",
        "tooltip-invert": "Cochez cette case pour cacher les modifications des pages dans l'espace de noms sélectionné (et l'espace de noms associé si coché)",
-       "tooltip-whatlinkshere-invert": "Vérifier cette boîte pour cacher les liens des pages sans espace de nom sélectionné.",
+       "tooltip-whatlinkshere-invert": "Cochez cette case pour cacher les liens des pages dans l'espace de nom sélectionné.",
        "namespace_association": "Espace de noms associé",
        "tooltip-namespace_association": "Cochez cette case pour inclure également l'espace de noms de discussion associé à l'espace de noms sélectionné",
        "blanknamespace": "(Principal)",
        "logentry-rights-rights-legacy": "$1 {{GENDER:$2|a modifié}} l'appartenance au groupe pour $3",
        "logentry-rights-autopromote": "$1 {{GENDER:$2|a été promu}} automatiquement de $4 à $5",
        "logentry-upload-upload": "$1 {{GENDER:$2|a téléchargé}} $3",
-       "logentry-upload-overwrite": "$1 {{GENDER:$2|a téléchargé}} une nouvelle version de $3",
+       "logentry-upload-overwrite": "$1 {{GENDER:$2|a téléversé}} une nouvelle version de $3",
        "logentry-upload-revert": "$1 {{GENDER:$2|a téléchargé}} $3",
        "rightsnone": "(aucun)",
        "revdelete-summary": "résumé de modification",
index 08f80e2..8cbba19 100644 (file)
        "uploaderror": "שגיאה בהעלאת הקובץ",
        "upload-recreate-warning": "'''אזהרה: קובץ בשם זה נמחק או הועבר.'''\n\nיומני המחיקות וההעברות של הדף מוצגים להלן:",
        "uploadtext": "השתמשו בטופס להלן כדי להעלות קבצים.\nכדי לראות או לחפש קבצים שהועלו בעבר אנא פנו ל[[Special:FileList|רשימת הקבצים שהועלו]], וכמו כן, העלאות (כולל העלאות של גרסה חדשה) מוצגות ב[[Special:Log/upload|יומן ההעלאות]], ומחיקות ב[[Special:Log/delete|יומן המחיקות]].\n\nכדי לכלול קובץ בדף, השתמשו בקישור באחת הצורות הבאות:\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.jpg]]</nowiki></code>''' לשימוש בגרסה המלאה של הקובץ\n* '''<code><nowiki>[[</nowiki>{{ns:file}}<nowiki>:File.png|200px|thumb|left|טקסט תיאור]]</nowiki></code>''' לשימוש בגרסה מוקטנת ברוחב 200 פיקסלים בתיבה בצד שמאל של הדף, עם 'טקסט תיאור' כתיאור\n* '''<code><nowiki>[[</nowiki>{{ns:media}}<nowiki>:File.ogg]]</nowiki></code>''' לקישור ישיר לקובץ בלי להציגו",
-       "upload-permitted": "סוגי קבצים מותרים: $1.",
-       "upload-preferred": "סוגי קבצים מומלצים: $1.",
-       "upload-prohibited": "סוגי קבצים אסורים: $1.",
+       "upload-permitted": "{{PLURAL:$2|סוג קובץ מותר|סוגי קבצים מותרים}}: $1.",
+       "upload-preferred": "{{PLURAL:$2|סוג קובץ מומלץ|סוגי קבצים מומלצים}}: $1.",
+       "upload-prohibited": "{{PLURAL:$2|סוג קובץ אסור|סוגי קבצים אסורים}}: $1.",
        "uploadlogpage": "יומן העלאות",
        "uploadlogpagetext": "להלן רשימה של העלאות הקבצים האחרונות שבוצעו.\nראו את [[Special:NewFiles|גלריית הקבצים החדשים]] להצגה ויזואלית שלהם.",
        "filename": "שם הקובץ",
index ec6b000..d6dc939 100644 (file)
        "deleteprotected": "Tu non pote deler iste pagina perque illo ha essite protegite.",
        "deleting-backlinks-warning": "'''Attention:''' Il ha [[Special:WhatLinksHere/{{FULLPAGENAME}}|altere paginas]] que liga a o transclude le pagina que tu es sur le puncto de deler.",
        "rollback": "Revocar modificationes",
-       "rollback_short": "Revocar",
        "rollbacklink": "revocar",
        "rollbacklinkcount": "revocar $1 {{PLURAL:$1|modification|modificationes}}",
        "rollbacklinkcount-morethan": "revocar plus de $1 {{PLURAL:$1|modification|modificationes}}",
        "namespace": "Spatio de nomine:",
        "invert": "Inverter selection",
        "tooltip-invert": "Marca iste quadrato pro celar le modificationes in paginas intra le spatio de nomines seligite (e le spatio de nomines associate, si tal option es seligite)",
+       "tooltip-whatlinkshere-invert": "Marca iste quadrato pro celar ligamines de paginas in le spatio de nomines seligite.",
        "namespace_association": "Spatio de nomines associate",
        "tooltip-namespace_association": "Marca iste quadrato pro includer anque le spatio de nomines de discussion o de subjecto associate al spatio de nomines seligite",
        "blanknamespace": "(Principal)",
        "javascripttest": "Test de JavaScript",
        "javascripttest-pagetext-noframework": "Iste pagina es reservate pro le execution de tests de JavaScript.",
        "javascripttest-pagetext-unknownframework": "Structura de test \"$1\" incognite.",
+       "javascripttest-pagetext-unknownaction": "Action \"$1\" incognite.",
        "javascripttest-pagetext-frameworks": "Per favor selige un del sequente structuras de test: $1",
        "javascripttest-pagetext-skins": "Selige un apparentia con le qual executar le tests:",
        "javascripttest-qunit-intro": "Vide [$1 documentation de tests] sur mediawiki.org.",
index 1a36523..13dc3b0 100644 (file)
        "namespace": "Namespace:",
        "invert": "Inverti selezione",
        "tooltip-invert": "Seleziona questa casella per nascondere le modifiche alle pagine all'interno del namespace selezionato (ed il namespace associato, se selezionato)",
+       "tooltip-whatlinkshere-invert": "Seleziona questa casella per nascondere i collegamenti dalle pagine all'interno del namespace selezionato",
        "namespace_association": "Namespace associato",
        "tooltip-namespace_association": "Seleziona questa casella per includere anche la pagina di discussione o l'oggetto del namespace associato con il namespace selezionato",
        "blanknamespace": "(Principale)",
index 295a854..1f3abea 100644 (file)
        "deletereasonotherlist": "다른 이유",
        "deletereason-dropdown": "* 일반적인 삭제 이유\n** 스팸\n** 문서 훼손 행위\n** 저작권 침해\n** 작성자의 요청\n** 깨진 넘겨주기",
        "delete-edit-reasonlist": "삭제 이유 편집",
-       "delete-toobig": "ì\9d´ ë¬¸ì\84\9cì\97\90ë\8a\94 {{PLURAL:$1|í\8e¸ì§\91 ì\97­ì\82¬}}ê°\80 $1ê°\9c ì\9e\88ì\8aµë\8b\88ë\8b¤.\ní\8e¸ì§\91 ì\97­ì\82¬ê°\80 ê¸´ ë¬¸ì\84\9c를 ì\82­ì \9cí\95\98ë©´ {{SITENAME}}ì\97\90 í\81° í\98¼ë\9e\80ì\9d\84 ì¤\84 ì\88\98 ì\9e\88기 ë\95\8c문ì\97\90 ì\82­ì \9cí\95  ì\88\98 ì\97\86ì\8aµ니다.",
+       "delete-toobig": "ì\9d´ ë¬¸ì\84\9cì\97\90ë\8a\94 {{PLURAL:$1|í\8e¸ì§\91 ì\97­ì\82¬}}ê°\80 $1ê°\9c ì\9d´ì\83\81 ì\9e\88ì\8aµë\8b\88ë\8b¤.\n{{SITENAME}}ì\97\90 ì\9d\98ë\8f\84í\95\98ì§\80 ì\95\8aì\9d\80 í\98¼ë\9e\80ì\9d\84 ì¤\84 ì\88\98 ì\9e\88기 ë\95\8c문ì\97\90 ì\9d´ë\9f° ë¬¸ì\84\9cì\9d\98 ì\82­ì \9cë\8a\94 ì \9cí\95\9cë\90©니다.",
        "delete-warning-toobig": "이 문서에는 {{PLURAL:$1|편집 역사}}가 $1개 있습니다.\n편집 역사가 긴 문서를 삭제하면 {{SITENAME}} 데이터베이스 동작에 큰 영향을 줄 수 있습니다.\n주의해 주세요.",
        "deleteprotected": "이 문서가 잠겨 있기 때문에 삭제할 수 없습니다.",
        "deleting-backlinks-warning": "'''경고:''' 삭제하려는 문서가 [[Special:WhatLinksHere/{{FULLPAGENAME}}|다른 문서]]에 링크되어 있거나 끼워져 있습니다.",
index 28f5b99..3bcd885 100644 (file)
        "pool-queuefull": "Kolejka zadań jest pełna",
        "pool-errorunknown": "Błąd nieznany",
        "pool-servererror": "Usługa licznika nie jest dostępna ($1).",
+       "poolcounter-usage-error": "Błąd użycia: $1",
        "aboutsite": "O {{GRAMMAR:MS.lp|{{SITENAME}}}}",
        "aboutpage": "Project:O {{GRAMMAR:MS.lp|{{SITENAME}}}}",
        "copyright": "Treść udostępniana na licencji $1, jeśli nie podano inaczej.",
index 28a989b..7f5ccb1 100644 (file)
        "namespace": "Domínio:",
        "invert": "Inverter seleção",
        "tooltip-invert": "Marque esta caixa para esconder as alterações a páginas no domínio selecionado (e no domínio associado, se escolheu fazê-lo)",
+       "tooltip-whatlinkshere-invert": "Marque esta caixa de seleção para ocultar ligações de páginas dentro do domínio selecionado.",
        "namespace_association": "Domínio associado",
        "tooltip-namespace_association": "Marque esta caixa para incluir também o domínio de conteúdo ou de discussão associado à sua seleção",
        "blanknamespace": "(Principal)",
index 9c7f5da..6a5cb68 100644 (file)
@@ -78,7 +78,7 @@
        "tog-watchdefault": "Lägg till sidor och filer jag redigerar i min bevakningslista",
        "tog-watchmoves": "Lägg till sidor och filer jag flyttar i min bevakningslista",
        "tog-watchdeletion": "Lägg till sidor och filer jag raderar i min bevakningslista",
-       "tog-watchrollback": "Lägg till sidor där jag har utfört en tillbakarullning till min övervakningslista",
+       "tog-watchrollback": "Lägg till sidor där jag har utfört en tillbakarullning till min bevakningslista",
        "tog-minordefault": "Markera automatiskt ändringar som mindre",
        "tog-previewontop": "Visa förhandsgranskningen ovanför redigeringsrutan",
        "tog-previewonfirst": "Visa förhandsgranskning vid första redigeringen",
index f885cd1..017f1d6 100644 (file)
@@ -21,7 +21,8 @@
                        "Kc kennylau",
                        "Mywood",
                        "Impersonator 1",
-                       "Cedric tsan cantonais"
+                       "Cedric tsan cantonais",
+                       "Liuxinyu970226"
                ]
        },
        "tog-underline": "連結加底線:",
        "content-model-text": "純文字",
        "content-model-javascript": "JavaScript程式語言",
        "content-model-css": "層疊樣式表",
-       "duplicate-args-category": "爾版用徂幾個重複加類嘅模",
+       "duplicate-args-category": "爾版用徂幾個重複加類嘅模",
        "expensive-parserfunction-warning": "警告: 呢一版有太多耗費嘅語法功能呼叫。\n\n佢應該少過$2次呼叫,佢而家係$1次呼叫。",
        "expensive-parserfunction-category": "響版度有太多嘅耗費嘅語法功能呼叫",
        "post-expand-template-inclusion-warning": "警告: 包含模大細太大。\n有啲模將唔會包含。",
index 7dc4afa..0792c24 100644 (file)
@@ -3,14 +3,14 @@
 <head>
        <meta charset="utf-8">
        <title>MediaWiki Code Example</title>
-       <script src="modules/startup.js"></script>
+       <script src="modules/src/startup.js"></script>
        <script>
                function startUp() {
                        mw.config = new mw.Map();
                }
        </script>
-       <script src="modules/jquery/jquery.js"></script>
-       <script src="modules/mediawiki/mediawiki.js"></script>
+       <script src="modules/lib/jquery/jquery.js"></script>
+       <script src="modules/src/mediawiki/mediawiki.js"></script>
        <style>
                .mw-jsduck-log {
                        position: relative;
@@ -78,7 +78,7 @@
                        eval( code );
                        callback && callback( true );
                } catch ( e ) {
-                       mw.log( 'Uncaught exception: ' + e );
+                       mw.log( 'Uncaught ' + e );
                        callback && callback( false, e );
                        throw e;
                }
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..101fcd9
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "name": "mediawiki",
+  "version": "0.0.0",
+  "scripts": {
+    "test": "grunt test"
+  },
+  "devDependencies": {
+    "grunt": "0.4.2",
+    "grunt-banana-checker": "0.2.0",
+    "grunt-contrib-jshint": "0.10.0",
+    "grunt-contrib-watch": "0.6.1",
+    "grunt-jscs": "0.8.1",
+    "grunt-jsonlint": "1.0.4",
+    "grunt-karma": "0.9.0",
+    "karma": "0.12.28",
+    "karma-chrome-launcher": "0.1.7",
+    "karma-firefox-launcher": "0.1.3",
+    "karma-qunit": "0.1.4",
+    "qunitjs": "1.15.0"
+  }
+}
index ad83e16..6e8cd99 100644 (file)
@@ -307,6 +307,7 @@ return array(
        ),
        'jquery.throttle-debounce' => array(
                'scripts' => 'resources/lib/jquery/jquery.ba-throttle-debounce.js',
+               'targets' => array( 'desktop', 'mobile' ),
        ),
        'jquery.validate' => array(
                'scripts' => 'resources/lib/jquery/jquery.validate.js',
index 52e0d4e..e66d8f6 100644 (file)
@@ -11,9 +11,6 @@
                fragment = null,
                shouldChangeFragment, index;
 
-       // Clear internal mw.config entries, so that no one tries to depend on them
-       mw.config.set( 'wgInternalRedirectTargetUrl', null );
-
        index = canonical.indexOf( '#' );
        if ( index !== -1 ) {
                fragment = canonical.slice( index );
index 1763c8e..c7858ab 100644 (file)
@@ -10,8 +10,6 @@
 ( function ( $ ) {
        'use strict';
 
-       /* Private Members */
-
        var mw,
                hasOwn = Object.prototype.hasOwnProperty,
                slice = Array.prototype.slice,
                trackQueue = [];
 
        /**
-        * Log a message to window.console, if possible. Useful to force logging of some
-        * errors that are otherwise hard to detect (I.e., this logs also in production mode).
-        * Gets console references in each invocation, so that delayed debugging tools work
-        * fine. No need for optimization here, which would only result in losing logs.
+        * Log a message to window.console, if possible.
+        *
+        * Useful to force logging of some  errors that are otherwise hard to detect (i.e., this logs
+        * also in production mode). Gets console references in each invocation instead of caching the
+        * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
         *
         * @private
         * @method log_
-        * @param {string} msg text for the log entry.
+        * @param {string} msg Text for the log entry.
         * @param {Error} [e]
         */
        function log( msg, e ) {
                var console = window.console;
                if ( console && console.log ) {
                        console.log( msg );
-                       // If we have an exception object, log it through .error() to trigger
-                       // proper stacktraces in browsers that support it. There are no (known)
-                       // browsers that don't support .error(), that do support .log() and
-                       // have useful exception handling through .log().
+                       // If we have an exception object, log it to the error channel to trigger a
+                       // proper stacktraces in browsers that support it. No fallback as we have no browsers
+                       // that don't support error(), but do support log().
                        if ( e && console.error ) {
                                console.error( String( e ), e );
                        }
                }
        }
 
-       // String format helper. Replaces $1, $2 .. $N placeholders with positional
-       // args. Used by Message.prototype.parser() and exported as mw.format().
-       function format( formatString ) {
-               var parameters = slice.call( arguments, 1 );
-               return formatString.replace( /\$(\d+)/g, function ( str, match ) {
-                       var index = parseInt( match, 10 ) - 1;
-                       return parameters[index] !== undefined ? parameters[index] : '$' + match;
-               } );
-       }
-
-       /* Object constructors */
-
        /**
-        * Creates an object that can be read from or written to from prototype functions
-        * that allow both single and multiple variables at once.
+        * Create an object that can be read from or written to from methods that allow
+        * interaction both with single and multiple properties at once.
         *
         *     @example
         *
-        *     var addies, wanted, results;
+        *     var collection, query, results;
         *
         *     // Create your address book
-        *     addies = new mw.Map();
+        *     collection = new mw.Map();
         *
         *     // This data could be coming from an external source (eg. API/AJAX)
-        *     addies.set( {
-        *         'John Doe' : '10 Wall Street, New York, USA',
-        *         'Jane Jackson' : '21 Oxford St, London, UK',
-        *         'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
+        *     collection.set( {
+        *         'John Doe': 'john@example.org',
+        *         'Jane Doe': 'jane@example.org',
+        *         'George van Halen': 'gvanhalen@example.org'
         *     } );
         *
-        *     wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
+        *     wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
         *
         *     // You can detect missing keys first
-        *     if ( !addies.exists( wanted ) ) {
-        *         // One or more are missing (in this case: "George Johnson")
+        *     if ( !collection.exists( wanted ) ) {
+        *         // One or more are missing (in this case: "Daniel Jackson")
         *         mw.log( 'One or more names were not found in your address book' );
         *     }
         *
-        *     // Or just let it give you what it can
-        *     results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
-        *     mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
-        *     mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
+        *     // Or just let it give you what it can. Optionally fill in from a default.
+        *     results = collection.get( wanted, 'nobody@example.com' );
+        *     mw.log( results['Jane Doe'] ); // "jane@example.org"
+        *     mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
         *
         * @class mw.Map
         *
         * @constructor
-        * @param {Object|boolean} [values] Value-bearing object to map, defaults to an empty object.
+        * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
+        *  empty object.
         *  For backwards-compatibility with mw.config, this can also be `true` in which case values
-        *  will be copied to the Window object as global variables (T72470). Values are copied in one
-        *  direction only. Changes to globals are not reflected in the map.
+        *  are copied to the Window object as global variables (T72470). Values are copied in
+        *  one direction only. Changes to globals are not reflected in the map.
         */
        function Map( values ) {
                if ( values === true ) {
 
        Map.prototype = {
                /**
-                * Get the value of one or multiple keys.
+                * Get the value of one or more keys.
                 *
-                * If called with no arguments, all values will be returned.
+                * If called with no arguments, all values are returned.
                 *
-                * @param {string|Array} selection String key or array of keys to get values for.
-                * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
-                * @return mixed If selection was a string returns the value or null,
-                *  If selection was an array, returns an object of key/values (value is null if not found),
-                *  If selection was not passed or invalid, will return the 'values' object member (be careful as
-                *  objects are always passed by reference in JavaScript!).
-                * @return {string|Object|null} Values as a string or object, null if invalid/inexistent.
+                * @param {string|Array} [selection] Key or array of keys to retrieve values for.
+                * @param {Mixed} [fallback=null] Value for keys that don't exist.
+                * @return {Mixed|Object| null} If selection was a string, returns the value,
+                *  If selection was an array, returns an object of key/values.
+                *  If no selection is passed, the 'values' container is returned. (Beware that,
+                *  as is the default in JavaScript, the object is returned by reference.)
                 */
                get: function ( selection, fallback ) {
                        var results, i;
                                return this.values;
                        }
 
-                       // invalid selection key
+                       // Invalid selection key
                        return null;
                },
 
                /**
-                * Sets one or multiple key/value pairs.
+                * Set one or more key/value pairs.
                 *
-                * @param {string|Object} selection String key to set value for, or object mapping keys to values.
+                * @param {string|Object} selection Key to set value for, or object mapping keys to values
                 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
-                * @return {boolean} This returns true on success, false on failure.
+                * @return {boolean} True on success, false on failure
                 */
                set: function ( selection, value ) {
                        var s;
                                }
                                return true;
                        }
-                       if ( typeof selection === 'string' && arguments.length ) {
+                       if ( typeof selection === 'string' && arguments.length > 1 ) {
                                this.values[selection] = value;
                                return true;
                        }
                },
 
                /**
-                * Checks if one or multiple keys exist.
+                * Check if one or more keys exist.
                 *
-                * @param {Mixed} selection String key or array of keys to check
-                * @return {boolean} Existence of key(s)
+                * @param {Mixed} selection Key or array of keys to check
+                * @return {boolean} True if the key(s) exist
                 */
                exists: function ( selection ) {
                        var s;
         * @class mw.Message
         *
         * @constructor
-        * @param {mw.Map} map Message storage
+        * @param {mw.Map} map Message store
         * @param {string} key
         * @param {Array} [parameters]
         */
 
        Message.prototype = {
                /**
-                * Simple message parser, does $N replacement and nothing else.
+                * Get parsed contents of the message.
                 *
+                * The default parser does simple $N replacements and nothing else.
                 * This may be overridden to provide a more complex message parser.
-                *
-                * The primary override is in mediawiki.jqueryMsg.
+                * The primary override is in the mediawiki.jqueryMsg module.
                 *
                 * This function will not be called for nonexistent messages.
+                *
+                * @return {string} Parsed message
                 */
                parser: function () {
-                       return format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
+                       return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
                },
 
                /**
-                * Appends (does not replace) parameters for replacement to the .parameters property.
+                * Add (does not replace) parameters for `N$` placeholder values.
                 *
                 * @param {Array} parameters
                 * @chainable
                },
 
                /**
-                * Converts message object to its string form based on the state of format.
+                * Convert message object to its string form based on current format.
                 *
-                * @return {string} Message as a string in the current form or `<key>` if key does not exist.
+                * @return {string} Message as a string in the current form, or `<key>` if key
+                *  does not exist.
                 */
                toString: function () {
                        var text;
                },
 
                /**
-                * Changes format to 'parse' and converts message to string
+                * Change format to 'parse' and convert message to string
                 *
                 * If jqueryMsg is loaded, this parses the message text from wikitext
                 * (where supported) to HTML
                },
 
                /**
-                * Changes format to 'plain' and converts message to string
+                * Change format to 'plain' and convert message to string
                 *
                 * This substitutes parameters, but otherwise does not change the
                 * message text.
                },
 
                /**
-                * Changes format to 'text' and converts message to string
+                * Change format to 'text' and convert message to string
                 *
                 * If jqueryMsg is loaded, {{-transformation is done where supported
                 * (such as {{plural:}}, {{gender:}}, {{int:}}).
                 *
-                * Otherwise, it is equivalent to plain.
+                * Otherwise, it is equivalent to plain
+                *
+                * @return {string} String form of text message
                 */
                text: function () {
                        this.format = 'text';
                },
 
                /**
-                * Changes the format to 'escaped' and converts message to string
+                * Change the format to 'escaped' and convert message to string
                 *
-                * This is equivalent to using the 'text' format (see text method), then
+                * This is equivalent to using the 'text' format (see #text), then
                 * HTML-escaping the output.
                 *
                 * @return {string} String form of html escaped message
                },
 
                /**
-                * Checks if message exists
+                * Check if a message exists
                 *
                 * @see mw.Map#exists
                 * @return {boolean}
         * @class mw
         */
        mw = {
-               /* Public Members */
 
                /**
                 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
                /**
                 * Format a string. Replace $1, $2 ... $N with positional arguments.
                 *
-                * @method
+                * Used by Message#parser().
+                *
                 * @since 1.25
                 * @param {string} fmt Format string
-                * @param {Mixed...} parameters Substitutions for $N placeholders.
+                * @param {Mixed...} parameters Values for $N replacements
                 * @return {string} Formatted string
                 */
-               format: format,
+               format: function ( formatString ) {
+                       var parameters = slice.call( arguments, 1 );
+                       return formatString.replace( /\$(\d+)/g, function ( str, match ) {
+                               var index = parseInt( match, 10 ) - 1;
+                               return parameters[index] !== undefined ? parameters[index] : '$' + match;
+                       } );
+               },
 
                /**
                 * Track an analytic event.
                },
 
                /**
-                * Register a handler for subset of analytic events, specified by topic
+                * Register a handler for subset of analytic events, specified by topic.
                 *
                 * Handlers will be called once for each tracked event, including any events that fired before the
                 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
                 *
                 * @param {string} topic Handle events whose name starts with this string prefix
                 * @param {Function} callback Handler to call for each matching tracked event
+                * @param {string} callback.topic
+                * @param {Object} [callback.data]
                 */
                trackSubscribe: function ( topic, callback ) {
                        var seen = 0;
                        } );
                },
 
-               // Make the Map constructor publicly available.
+               // Expose Map constructor
                Map: Map,
 
-               // Make the Message constructor publicly available.
+               // Expose Message constructor
                Message: Message,
 
                /**
-                * Map of configuration values
+                * Map of configuration values.
                 *
                 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
                 * on mediawiki.org.
                 *
                 * @property {mw.Map} config
                 */
-               // Dummy placeholder. Re-assigned in ResourceLoaderStartUpModule to an instance of `mw.Map`.
+               // Dummy placeholder later assigned in ResourceLoaderStartUpModule
                config: null,
 
                /**
                 * Empty object that plugins can be installed in.
+                *
                 * @property
                 */
                libs: {},
                legacy: {},
 
                /**
-                * Localization system
+                * Store for messages.
+                *
                 * @property {mw.Map}
                 */
                messages: new Map(),
 
                /**
-                * Templates associated with a module
+                * Store for templates associated with a module.
+                *
                 * @property {mw.Map}
                 */
                templates: new Map(),
 
-               /* Public Methods */
-
                /**
                 * Get a message object.
                 *
                 *
                 * @see mw.Message
                 * @param {string} key Key of message to get
-                * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+                * @param {Mixed...} parameters Values for $N replacements
                 * @return {mw.Message}
                 */
                message: function ( key ) {
-                       // Variadic arguments
                        var parameters = slice.call( arguments, 1 );
                        return new Message( mw.messages, key, parameters );
                },
                 *
                 * @see mw.Message
                 * @param {string} key Key of message to get
-                * @param {Mixed...} parameters Parameters for the $N replacements in messages.
+                * @param {Mixed...} parameters Values for $N replacements
                 * @return {string}
                 */
                msg: function () {
                        /**
                         * Write a message the console's warning channel.
                         * Also logs a stacktrace for easier debugging.
-                        * Each action is silently ignored if the browser doesn't support it.
+                        * Actions not supported by the browser console are silently ignored.
                         *
                         * @param {string...} msg Messages to output to console
                         */
                         * @param {Object} obj Host object of deprecated property
                         * @param {string} key Name of property to create in `obj`
                         * @param {Mixed} val The value this property should return when accessed
-                        * @param {string} [msg] Optional text to include in the deprecation message.
+                        * @param {string} [msg] Optional text to include in the deprecation message
                         */
                        log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
                                obj[key] = val;
                        } : function ( obj, key, val, msg ) {
                                msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
+                               // Support: IE8
+                               // Can throw on Object.defineProperty.
                                try {
                                        Object.defineProperty( obj, key, {
                                                configurable: true,
                                                }
                                        } );
                                } catch ( err ) {
-                                       // IE8 can throw on Object.defineProperty
-                                       // Create a copy of the value to the object.
+                                       // Fallback to creating a copy of the value to the object.
                                        obj[key] = val;
                                }
                        };
                }() ),
 
                /**
-                * Client-side module loader which integrates with the MediaWiki ResourceLoader
+                * Client for ResourceLoader server end point.
+                *
+                * This client is in charge of maintaining the module registry and state
+                * machine, initiating network (batch) requests for loading modules, as
+                * well as dependency resolution and execution of source code.
+                *
+                * For more information, refer to
+                * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
+                *
                 * @class mw.loader
                 * @singleton
                 */
                loader: ( function () {
 
-                       /* Private Members */
-
                        /**
-                        * Mapping of registered modules
+                        * Mapping of registered modules.
                         *
-                        * The jquery module is pre-registered, because it must have already
-                        * been provided for this object to have been built, and in debug mode
-                        * jquery would have been provided through a unique loader request,
-                        * making it impossible to hold back registration of jquery until after
-                        * mediawiki.
-                        *
-                        * For exact details on support for script, style and messages, look at
-                        * mw.loader.implement.
+                        * See #implement for exact details on support for script, style and messages.
                         *
                         * Format:
+                        *
                         *     {
                         *         'moduleName': {
-                        *             // At registry
-                        *             'version': ############## (unix timestamp),
+                        *             // From startup mdoule
+                        *             'version': ############## (unix timestamp)
                         *             'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
-                        *             'group': 'somegroup', (or) null,
-                        *             'source': 'local', 'someforeignwiki', (or) null
-                        *             'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
+                        *             'group': 'somegroup', (or) null
+                        *             'source': 'local', (or) 'anotherwiki'
                         *             'skip': 'return !!window.Example', (or) null
+                        *             'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
                         *
                         *             // Added during implementation
-                        *             'skipped': true,
-                        *             'script': ...,
-                        *             'style': ...,
-                        *             'messages': { 'key': 'value' },
+                        *             'skipped': true
+                        *             'script': ...
+                        *             'style': ...
+                        *             'messages': { 'key': 'value' }
                         *         }
                         *     }
                         *
                         * @private
                         */
                        var registry = {},
-                               //
                                // Mapping of sources, keyed by source-id, values are strings.
+                               //
                                // Format:
-                               //      {
-                               //              'sourceId': 'http://foo.bar/w/load.php'
-                               //      }
+                               //
+                               //     {
+                               //         'sourceId': 'http://example.org/w/load.php'
+                               //     }
                                //
                                sources = {},
+
                                // List of modules which will be loaded as when ready
                                batch = [],
+
                                // List of modules to be loaded
                                queue = [],
+
                                // List of callback functions waiting for modules to be ready to be called
                                jobs = [],
+
                                // Selector cache for the marker element. Use getMarker() to get/use the marker!
                                $marker = null,
-                               // Buffer for addEmbeddedCSS.
+
+                               // Buffer for #addEmbeddedCSS
                                cssBuffer = '',
-                               // Callbacks for addEmbeddedCSS.
-                               cssCallbacks = $.Callbacks();
 
-                       /* Private methods */
+                               // Callbacks for #addEmbeddedCSS
+                               cssCallbacks = $.Callbacks();
 
                        function getMarker() {
-                               // Cached
                                if ( !$marker ) {
+                                       // Cache
                                        $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
                                        if ( !$marker.length ) {
                                                mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
                        }
 
                        /**
-                        * Create a new style tag and add it to the DOM.
+                        * Create a new style element and add it to the DOM.
                         *
                         * @private
                         * @param {string} text CSS text
-                        * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
-                        *  inserted before. Otherwise it will be appended to `<head>`.
-                        * @return {HTMLElement} Reference to the created `<style>` element.
+                        * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
+                        *  should be inserted before
+                        * @return {HTMLElement} Reference to the created style element
                         */
                        function newStyleTag( text, nextnode ) {
                                var s = document.createElement( 'style' );
-                               // Insert into document before setting cssText (bug 33305)
+                               // Support: IE
+                               // Must attach to document before setting cssText (bug 33305)
                                if ( nextnode ) {
-                                       // Must be inserted with native insertBefore, not $.fn.before.
-                                       // When using jQuery to insert it, like $nextnode.before( s ),
-                                       // then IE6 will throw "Access is denied" when trying to append
-                                       // to .cssText later. Some kind of weird security measure.
-                                       // http://stackoverflow.com/q/12586482/319266
-                                       // Works: jsfiddle.net/zJzMy/1
-                                       // Fails: jsfiddle.net/uJTQz
-                                       // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
-                                       if ( nextnode.jquery ) {
-                                               nextnode = nextnode.get( 0 );
-                                       }
-                                       nextnode.parentNode.insertBefore( s, nextnode );
+                                       $( nextnode ).before( s );
                                } else {
                                        document.getElementsByTagName( 'head' )[0].appendChild( s );
                                }
                                if ( s.styleSheet ) {
-                                       // IE
+                                       // Support: IE6-10
+                                       // Old IE ignores appended text nodes, access stylesheet directly.
                                        s.styleSheet.cssText = text;
                                } else {
-                                       // Other browsers.
-                                       // (Safari sometimes borks on non-string values,
-                                       // play safe by casting to a string, just in case.)
-                                       s.appendChild( document.createTextNode( String( text ) ) );
+                                       // Standard behaviour
+                                       s.appendChild( document.createTextNode( text ) );
                                }
                                return s;
                        }
 
                        /**
-                        * Checks whether it is safe to add this css to a stylesheet.
+                        * Check whether given styles are safe to to a stylesheet.
                         *
                         * @private
                         * @param {string} cssText
                                // Yield once before inserting the <style> tag. There are likely
                                // more calls coming up which we can combine this way.
                                // Appending a stylesheet and waiting for the browser to repaint
-                               // is fairly expensive, this reduces it (bug 45810)
+                               // is fairly expensive, this reduces that (bug 45810)
                                if ( cssText ) {
                                        // Be careful not to extend the buffer with css that needs a new stylesheet
                                        if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
                                                cssBuffer += '\n' + cssText;
                                                // TODO: Use requestAnimationFrame in the future which will
                                                // perform even better by not injecting styles while the browser
-                                               // is paiting.
+                                               // is painting.
                                                setTimeout( function () {
                                                        // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
                                                        // (below version 13) has the non-standard behaviour of passing a
                                } else if ( cssBuffer ) {
                                        cssText = cssBuffer;
                                        cssBuffer = '';
+
                                } else {
-                                       // This is a delayed call, but buffer is already cleared by
+                                       // This is a delayed call, but buffer was already cleared by
                                        // another delayed call.
                                        return;
                                }
                                if ( 'documentMode' in document && document.documentMode <= 9 ) {
 
                                        $style = getMarker().prev();
-                                       // Verify that the element before Marker actually is a
+                                       // Verify that the element before the marker actually is a
                                        // <style> tag and one that came from ResourceLoader
                                        // (not some other style tag or even a `<meta>` or `<script>`).
                                        if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
                                                // There's already a dynamic <style> tag present and
                                                // canExpandStylesheetWith() gave a green light to append more to it.
                                                styleEl = $style.get( 0 );
+                                               // Support: IE6-10
                                                if ( styleEl.styleSheet ) {
                                                        try {
-                                                               styleEl.styleSheet.cssText += cssText; // IE
+                                                               styleEl.styleSheet.cssText += cssText;
                                                        } catch ( e ) {
                                                                log( 'Stylesheet error', e );
                                                        }
                                                } else {
-                                                       styleEl.appendChild( document.createTextNode( String( cssText ) ) );
+                                                       styleEl.appendChild( document.createTextNode( cssText ) );
                                                }
                                                cssCallbacks.fire().empty();
                                                return;
                        }
 
                        /**
-                        * Convert UNIX timestamp to ISO8601 format
-                        * @param {number} timestamp UNIX timestamp
+                        * Zero-pad three numbers.
+                        *
                         * @private
+                        * @param {number} a
+                        * @param {number} b
+                        * @param {number} c
+                        * @return {string}
+                        */
+                       function pad( a, b, c ) {
+                               return [
+                                       a < 10 ? '0' + a : a,
+                                       b < 10 ? '0' + b : b,
+                                       c < 10 ? '0' + c : c
+                               ].join( '' );
+                       }
+
+                       /**
+                        * Convert UNIX timestamp to ISO8601 format.
+                        *
+                        * @private
+                        * @param {number} timestamp UNIX timestamp
                         */
                        function formatVersionNumber( timestamp ) {
                                var     d = new Date();
-                               function pad( a, b, c ) {
-                                       return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
-                               }
                                d.setTime( timestamp * 1000 );
                                return [
-                                       pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
-                                       pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
+                                       pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
+                                       'T',
+                                       pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
+                                       'Z'
                                ].join( '' );
                        }
 
                        /**
-                        * Resolves dependencies and detects circular references.
+                        * Resolve dependencies and detect circular references.
                         *
                         * @private
                         * @param {string} module Name of the top-level module whose dependencies shall be
-                        *   resolved and sorted.
+                        *  resolved and sorted.
                         * @param {Array} resolved Returns a topological sort of the given module and its
-                        *   dependencies, such that later modules depend on earlier modules. The array
-                        *   contains the module names. If the array contains already some module names,
-                        *   this function appends its result to the pre-existing array.
+                        *  dependencies, such that later modules depend on earlier modules. The array
+                        *  contains the module names. If the array contains already some module names,
+                        *  this function appends its result to the pre-existing array.
                         * @param {Object} [unresolved] Hash used to track the current dependency
-                        *   chain; used to report loops in the dependency graph.
+                        *  chain; used to report loops in the dependency graph.
                         * @throws {Error} If any unregistered module or a dependency loop is encountered
                         */
                        function sortDependencies( module, resolved, unresolved ) {
                                        }
                                }
                                if ( $.inArray( module, resolved ) !== -1 ) {
-                                       // Module already resolved; nothing to do.
+                                       // Module already resolved; nothing to do
                                        return;
                                }
-                               // unresolved is optional, supply it if not passed in
+                               // Create unresolved if not passed in
                                if ( !unresolved ) {
                                        unresolved = {};
                                }
                        }
 
                        /**
-                        * Gets a list of module names that a module depends on in their proper dependency
+                        * Get a list of module names that a module depends on in their proper dependency
                         * order.
                         *
                         * @private
                         * @param {string} module Module name or array of string module names
-                        * @return {Array} list of dependencies, including 'module'.
+                        * @return {Array} List of dependencies, including 'module'.
                         * @throws {Error} If circular reference is detected
                         */
                        function resolve( module ) {
                        }
 
                        /**
-                        * Narrows a list of module names down to those matching a specific
-                        * state (see comment on top of this scope for a list of valid states).
+                        * Narrow down a list of module names to those matching a specific
+                        * state (see #registry for a list of valid states).
+                        *
                         * One can also filter for 'unregistered', which will return the
                         * modules names that don't have a registry entry.
                         *
                         * @private
                         * @param {string|string[]} states Module states to filter by
-                        * @param {Array} [modules] List of module names to filter (optional, by default the entire
-                        * registry is used)
+                        * @param {Array} [modules] List of module names to filter (optional, by default the
+                        * entire registry is used)
                         * @return {Array} List of filtered module names
                         */
                        function filter( states, modules ) {
                        }
 
                        /**
-                        * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
-                        * and modules that depend upon this module. if the given module failed, propagate the 'error'
-                        * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
-                        * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
+                        * A module has entered state 'ready', 'error', or 'missing'. Automatically update
+                        * pending jobs and modules that depend upon this module. If the given module failed,
+                        * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
+                        * all jobs/modules now having their dependencies satisfied.
+                        *
+                        * Jobs that depend on a failed module, will have their error callback ran (if any).
                         *
                         * @private
                         * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
                        function handlePending( module ) {
                                var j, job, hasErrors, m, stateChange;
 
-                               // Modules.
                                if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
                                        // If the current module failed, mark all dependent modules also as failed.
                                        // Iterate until steady-state to propagate the error state upwards in the
                                                stateChange = false;
                                                for ( m in registry ) {
                                                        if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
-                                                               if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
+                                                               if ( filter( ['error', 'missing'], registry[m].dependencies ).length ) {
                                                                        registry[m].state = 'error';
                                                                        stateChange = true;
                                                                }
                                 */
                                function addLink( media, url ) {
                                        var el = document.createElement( 'link' );
-                                       // For IE: Insert in document *before* setting href
+                                       // Support: IE
+                                       // Insert in document *before* setting href
                                        getMarker().before( el );
                                        el.rel = 'stylesheet';
                                        if ( media && media !== 'all' ) {
                                        currReqBase
                                );
                                request = sortQuery( request );
-                               // Append &* to avoid triggering the IE6 extension check
+                               // Support: IE6
+                               // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
+                               // isn't actually used in IE6, but MediaWiki enforces it in general.
                                addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
                        }
 
                                 * the modules are registered.
                                 *
                                 * @param {string|Array} module Module name or array of arrays, each containing
-                                *   a list of arguments compatible with this method
+                                *  a list of arguments compatible with this method
                                 * @param {number} version Module version number as a timestamp (falls backs to 0)
                                 * @param {string|Array|Function} dependencies One string or array of strings of module
                                 *  names on which this module depends, or a function that returns that array.
                                        }
                                        // Allow calling with an external url or single dependency as a string
                                        if ( typeof modules === 'string' ) {
-                                               // Support adding arbitrary external scripts
                                                if ( /^(https?:)?\/\//.test( modules ) ) {
                                                        if ( async === undefined ) {
                                                                // Assume async for bug 34542
                                                                async = true;
                                                        }
                                                        if ( type === 'text/css' ) {
-                                                               // IE7-8 throws security warnings when inserting a <link> tag
-                                                               // with a protocol-relative URL set though attributes (instead of
-                                                               // properties) - when on HTTPS. See also bug 41331.
+                                                               // Support: IE 7-8
+                                                               // Use properties instead of attributes as IE throws security
+                                                               // warnings when inserting a <link> tag with a protocol-relative
+                                                               // URL set though attributes - when on HTTPS. See bug 41331.
                                                                l = document.createElement( 'link' );
                                                                l.rel = 'stylesheet';
                                                                l.href = modules;
                                        },
 
                                        /**
-                                        * Get a string key on which to vary the module cache.
+                                        * Get a key on which to vary the module cache.
                                         * @return {string} String of concatenated vary conditions.
                                         */
                                        getVary: function () {
                                        },
 
                                        /**
-                                        * Get a string key for a specific module. The key format is '[name]@[version]'.
+                                        * Get a key for a specific module. The key format is '[name]@[version]'.
                                         *
                                         * @param {string} module Module name
                                         * @return {string|null} Module key or null if module does not exist
                                 *  - null or undefined: The short closing form is used, e.g. `<br/>`.
                                 *  - this.Raw: The value attribute is included without escaping.
                                 *  - this.Cdata: The value attribute is included, and an exception is
-                                *   thrown if it contains an illegal ETAGO delimiter.
-                                *   See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
+                                *    thrown if it contains an illegal ETAGO delimiter.
+                                *    See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
                                 * @return {string} HTML
                                 */
                                element: function ( name, attrs, contents ) {
diff --git a/tests/frontend/Gruntfile.js b/tests/frontend/Gruntfile.js
deleted file mode 100644 (file)
index fd89e56..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-/*!
- * Grunt file
- */
-
-/*jshint node:true */
-module.exports = function ( grunt ) {
-       grunt.loadNpmTasks( 'grunt-contrib-jshint' );
-       grunt.loadNpmTasks( 'grunt-contrib-watch' );
-       grunt.loadNpmTasks( 'grunt-banana-checker' );
-       grunt.loadNpmTasks( 'grunt-jscs' );
-       grunt.loadNpmTasks( 'grunt-jsonlint' );
-       grunt.loadNpmTasks( 'grunt-karma' );
-
-       grunt.file.setBase(  __dirname + '/../..' );
-
-       var wgServer = process.env.MW_SERVER,
-               wgScriptPath = process.env.MW_SCRIPT_PATH;
-
-       grunt.initConfig( {
-               pkg: grunt.file.readJSON( __dirname + '/package.json' ),
-               jshint: {
-                       options: {
-                               jshintrc: true
-                       },
-                       all: [
-                               '*.js',
-                               '{includes,languages,resources,skins,tests}/**/*.js'
-                       ]
-               },
-               jscs: {
-                       all: [
-                               '<%= jshint.all %>',
-                               // Auto-generated file with JSON (double quotes)
-                               '!tests/qunit/data/mediawiki.jqueryMsg.data.js',
-                               // Skip functions are stored as script files but wrapped in a function when
-                               // executed. node-jscs trips on the would-be "Illegal return statement".
-                               '!resources/src/*-skip.js'
-
-                       // Exclude all files ignored by jshint
-                       ].concat( grunt.file.read( '.jshintignore' ).split( '\n' ).reduce( function ( patterns, pattern ) {
-                               // Filter out empty lines
-                               if ( pattern.length && pattern[0] !== '#' ) {
-                                       patterns.push( '!' + pattern );
-                               }
-                               return patterns;
-                       }, [] ) )
-               },
-               jsonlint: {
-                       all: [
-                               '.jscsrc',
-                               '{languages,maintenance,resources}/**/*.json',
-                               'tests/frontend/package.json'
-                       ]
-               },
-               banana: {
-                       core: 'languages/i18n/',
-                       api: 'includes/api/i18n/',
-                       installer: 'includes/installer/i18n/'
-               },
-               watch: {
-                       files: [
-                               '<%= jscs.all %>',
-                               '<%= jsonlint.all %>',
-                               '.jshintignore',
-                               '.jshintrc'
-                       ],
-                       tasks: 'test'
-               },
-               karma: {
-                       options: {
-                               proxies: ( function () {
-                                       var obj = {};
-                                       // Set up a proxy for requests to relative urls inside wgScriptPath. Uses a
-                                       // property accessor instead of plain obj[wgScriptPath] assignment as throw if
-                                       // unset. Running grunt normally (e.g. npm test), should not fail over this.
-                                       // This ensures 'npm test' works out of the box, statically, on a git clone
-                                       // without MediaWiki fully installed or some environment variables set.
-                                       Object.defineProperty( obj, wgScriptPath, {
-                                               enumerable: true,
-                                               get: function () {
-                                                       if ( !wgServer ) {
-                                                               grunt.fail.fatal( 'MW_SERVER is not set' );
-                                                       }
-                                                       if ( !wgScriptPath ) {
-                                                               grunt.fail.fatal( 'MW_SCRIPT_PATH is not set' );
-                                                       }
-                                                       return wgServer + wgScriptPath;
-                                               }
-                                       } );
-                                       return obj;
-                               }() ),
-                               files: [ {
-                                       pattern: wgServer + wgScriptPath + '/index.php?title=Special:JavaScriptTest/qunit/export',
-                                       watched: false,
-                                       included: true,
-                                       served: false
-                               } ],
-                               frameworks: [ 'qunit' ],
-                               reporters: [ 'dots' ],
-                               singleRun: true,
-                               autoWatch: false
-                       },
-                       main: {
-                               browsers: [ 'Chrome' ]
-                       },
-                       more: {
-                               browsers: [ 'Chrome', 'Firefox' ]
-                       }
-               }
-       } );
-
-       grunt.registerTask( 'lint', ['jshint', 'jscs', 'jsonlint', 'banana'] );
-       grunt.registerTask( 'qunit', 'karma:main' );
-
-       grunt.registerTask( 'test', ['lint'] );
-       grunt.registerTask( 'default', 'test' );
-};
diff --git a/tests/frontend/package.json b/tests/frontend/package.json
deleted file mode 100644 (file)
index 101fcd9..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-  "name": "mediawiki",
-  "version": "0.0.0",
-  "scripts": {
-    "test": "grunt test"
-  },
-  "devDependencies": {
-    "grunt": "0.4.2",
-    "grunt-banana-checker": "0.2.0",
-    "grunt-contrib-jshint": "0.10.0",
-    "grunt-contrib-watch": "0.6.1",
-    "grunt-jscs": "0.8.1",
-    "grunt-jsonlint": "1.0.4",
-    "grunt-karma": "0.9.0",
-    "karma": "0.12.28",
-    "karma-chrome-launcher": "0.1.7",
-    "karma-firefox-launcher": "0.1.3",
-    "karma-qunit": "0.1.4",
-    "qunitjs": "1.15.0"
-  }
-}
index 6c8c62f..ee33ade 100644 (file)
@@ -1,6 +1,8 @@
 /*jshint -W024 */
 ( function ( mw, $ ) {
-       var specialCharactersPageName;
+       var specialCharactersPageName,
+               // Can't mock SITENAME since jqueryMsg caches it at load
+               siteName = mw.config.get( 'wgSiteName' );
 
        // Since QUnitTestResources.php loads both mediawiki and mediawiki.jqueryMsg as
        // dependencies, this only tests the monkey-patched behavior with the two of them combined.
@@ -55,7 +57,7 @@
                this.restoreWarnings();
        } );
 
-       QUnit.test( 'mw.Map', 34, function ( assert ) {
+       QUnit.test( 'mw.Map', 35, function ( assert ) {
                var arry, conf, funky, globalConf, nummy, someValues;
 
                conf = new mw.Map();
                assert.strictEqual( conf.set( 'constructor', 42 ), true, 'Map.set for key "constructor"' );
                assert.strictEqual( conf.get( 'constructor' ), 42, 'Map.get for key "constructor"' );
 
-               assert.strictEqual( conf.set( 'ImUndefined', undefined ), true, 'Map.set allows setting value to `undefined`' );
-               assert.equal( conf.get( 'ImUndefined', 'fallback' ), undefined, 'Map.get supports retreiving value of `undefined`' );
+               assert.strictEqual( conf.set( 'undef' ), false, 'Map.set requires explicit value (no undefined default)' );
+
+               assert.strictEqual( conf.set( 'undef', undefined ), true, 'Map.set allows setting value to `undefined`' );
+               assert.equal( conf.get( 'undef', 'fallback' ), undefined, 'Map.get supports retreiving value of `undefined`' );
 
                assert.strictEqual( conf.set( funky, 'Funky' ), false, 'Map.set returns boolean false if key was invalid (Function)' );
                assert.strictEqual( conf.set( arry, 'Arry' ), false, 'Map.set returns boolean false if key was invalid (Array)' );
                conf.set( String( nummy ), 'I used to be a number' );
 
                assert.strictEqual( conf.exists( 'doesNotExist' ), false, 'Map.exists where property does not exist' );
-               assert.strictEqual( conf.exists( 'ImUndefined' ), true, 'Map.exists where value is `undefined`' );
+               assert.strictEqual( conf.exists( 'undef' ), true, 'Map.exists where value is `undefined`' );
                assert.strictEqual( conf.exists( nummy ), false, 'Map.exists where key is invalid but looks like an existing key' );
 
                // Multiple values at once
                assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
 
                assert.ok( mw.messages.set( 'multiple-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ), 'mw.messages.set: Register' );
-               assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message', 'Curly brace format works correctly' );
+               assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + siteName + '" is the home of Other Message', 'Curly brace format works correctly' );
                assert.equal( mw.message( 'multiple-curly-brace' ).plain(), mw.messages.get( 'multiple-curly-brace' ), 'Plain format works correctly for curly brace message' );
-               assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+               assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + siteName + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
 
                assert.ok( mw.messages.set( 'multiple-square-brackets-and-ampersand', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ), 'mw.messages.set: Register' );
                assertMultipleFormats( ['multiple-square-brackets-and-ampersand'], ['plain', 'text'], mw.messages.get( 'multiple-square-brackets-and-ampersand' ), 'Square bracket message is not processed' );
                assert.equal( mw.message( 'gender-plural-msg', 'male', 1 ).plain(), '{{GENDER:male|he|she|they}} {{PLURAL:1|is|are}} awesome', 'Parameters are substituted, but gender and plural are not resolved in plain mode' );
 
                assert.equal( mw.message( 'grammar-msg' ).plain(), mw.messages.get( 'grammar-msg' ), 'Grammar is not resolved in plain mode' );
-               assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
-               assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + mw.html.escape( mw.config.get( 'wgSiteName' ) ), 'Grammar is resolved in escaped mode' );
+               assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + siteName, 'Grammar is resolved' );
+               assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + siteName, 'Grammar is resolved in escaped mode' );
 
                assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987,654,321.654', 'formatnum is resolved' );
                assert.equal( mw.message( 'formatnum-msg' ).plain(), mw.messages.get( 'formatnum-msg' ), 'formatnum is not resolved in plain mode' );
                assert.equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' );
                assert.equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' );
 
-               assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
+               assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + siteName, 'Grammar is resolved' );
 
                assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987,654,321.654', 'formatnum is resolved' );