Merge "Initial support for Content Security Policy, disabled by default"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 14 May 2018 04:17:15 +0000 (04:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 14 May 2018 04:17:15 +0000 (04:17 +0000)
179 files changed:
README [deleted file]
README.md [new file with mode: 0644]
README.mediawiki [deleted symlink]
autoload.php
composer.json
includes/DefaultSettings.php
includes/api/i18n/he.json
includes/collation/IcuCollation.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/logging/LogEventsList.php
includes/logging/LogPager.php
includes/search/SearchEngine.php
includes/specials/SpecialPreferences.php
languages/i18n/as.json
languages/i18n/azb.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/ca.json
languages/i18n/ckb.json
languages/i18n/cs.json
languages/i18n/cv.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gcr.json
languages/i18n/gl.json
languages/i18n/he.json
languages/i18n/hu.json
languages/i18n/ia.json
languages/i18n/ig.json
languages/i18n/inh.json
languages/i18n/io.json
languages/i18n/ja.json
languages/i18n/jv.json
languages/i18n/lb.json
languages/i18n/lfn.json
languages/i18n/mk.json
languages/i18n/nl.json
languages/i18n/oc.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/ru.json
languages/i18n/skr-arab.json
languages/i18n/sl.json
languages/i18n/sv.json
languages/i18n/szl.json
languages/i18n/tr.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/categoryChangesAsRdf.php [new file with mode: 0644]
package.json
resources/Resources.php
resources/src/mediawiki.language/mediawiki.language.init.js
resources/src/mediawiki.skinning/content.parsoid.less
resources/src/mediawiki.special.apisandbox.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.apisandbox/apisandbox.css [new file with mode: 0644]
resources/src/mediawiki.special.apisandbox/apisandbox.js [new file with mode: 0644]
resources/src/mediawiki.special.block.js [new file with mode: 0644]
resources/src/mediawiki.special.changecredentials.js [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.css [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.enhanced.css [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.legend.css [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.legend.js [new file with mode: 0644]
resources/src/mediawiki.special.changeslist.visitedstatus.js [new file with mode: 0644]
resources/src/mediawiki.special.comparepages.styles.less [new file with mode: 0644]
resources/src/mediawiki.special.contributions.js [new file with mode: 0644]
resources/src/mediawiki.special.edittags.js [new file with mode: 0644]
resources/src/mediawiki.special.edittags.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.import.js [new file with mode: 0644]
resources/src/mediawiki.special.movePage.css [new file with mode: 0644]
resources/src/mediawiki.special.movePage.js [new file with mode: 0644]
resources/src/mediawiki.special.pageLanguage.js [new file with mode: 0644]
resources/src/mediawiki.special.pagesWithProp.css [new file with mode: 0644]
resources/src/mediawiki.special.preferences.ooui/editfont.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences.ooui/tabs.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.preferences.styles.ooui.css [new file with mode: 0644]
resources/src/mediawiki.special.preferences/confirmClose.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/convertmessagebox.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/personalEmail.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/tabs.legacy.js [new file with mode: 0644]
resources/src/mediawiki.special.preferences/timezone.js [new file with mode: 0644]
resources/src/mediawiki.special.recentchanges.js [new file with mode: 0644]
resources/src/mediawiki.special.revisionDelete.js [new file with mode: 0644]
resources/src/mediawiki.special.search.commonsInterwikiWidget.js [new file with mode: 0644]
resources/src/mediawiki.special.search.interwikiwidget.styles.less [new file with mode: 0644]
resources/src/mediawiki.special.search.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.search/search.css [new file with mode: 0644]
resources/src/mediawiki.special.search/search.js [new file with mode: 0644]
resources/src/mediawiki.special.undelete.js [new file with mode: 0644]
resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css [new file with mode: 0644]
resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js [new file with mode: 0644]
resources/src/mediawiki.special.upload.styles.css [new file with mode: 0644]
resources/src/mediawiki.special.upload/templates/thumbnail.html [new file with mode: 0644]
resources/src/mediawiki.special.upload/upload.js [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.common.styles/userlogin.css [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.login.styles/login.css [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.js [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png [new file with mode: 0644]
resources/src/mediawiki.special.userlogin.signup.styles/signup.css [new file with mode: 0644]
resources/src/mediawiki.special.userrights.js [new file with mode: 0644]
resources/src/mediawiki.special.version.css [new file with mode: 0644]
resources/src/mediawiki.special.watchlist.js [new file with mode: 0644]
resources/src/mediawiki.special.watchlist.styles.css [new file with mode: 0644]
resources/src/mediawiki.special/images/glyph-people-large.png [deleted file]
resources/src/mediawiki.special/images/icon-contributors.png [deleted file]
resources/src/mediawiki.special/images/icon-edits.png [deleted file]
resources/src/mediawiki.special/images/icon-lock.png [deleted file]
resources/src/mediawiki.special/images/icon-pages.png [deleted file]
resources/src/mediawiki.special/mediawiki.special.apisandbox.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.apisandbox.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.block.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.changecredentials.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less [deleted file]
resources/src/mediawiki.special/mediawiki.special.contributions.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.edittags.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.edittags.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.import.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.movePage.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.movePage.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.pageLanguage.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.recentchanges.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.revisionDelete.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.interwikiwidget.styles.less [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.search.styles.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.undelete.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.upload.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.upload.styles.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.common.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.login.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.userrights.js [deleted file]
resources/src/mediawiki.special/mediawiki.special.version.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.watchlist.css [deleted file]
resources/src/mediawiki.special/mediawiki.special.watchlist.js [deleted file]
resources/src/mediawiki.special/templates/thumbnail.html [deleted file]
resources/src/mediawiki/mediawiki.js
tests/phpunit/data/categoriesrdf/change.sparql [new file with mode: 0644]
tests/phpunit/data/categoriesrdf/delete.sparql [new file with mode: 0644]
tests/phpunit/data/categoriesrdf/move.sparql [new file with mode: 0644]
tests/phpunit/data/categoriesrdf/new.sparql [new file with mode: 0644]
tests/phpunit/data/categoriesrdf/restore.sparql [new file with mode: 0644]
tests/phpunit/data/categoriesrdf/updatets.txt [new file with mode: 0644]
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/maintenance/categoryChangesRdfTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.test.js

diff --git a/README b/README
deleted file mode 100644 (file)
index ad9b9d9..0000000
--- a/README
+++ /dev/null
@@ -1,33 +0,0 @@
-== MediaWiki ==
-
-MediaWiki is a free and open-source wiki software package written in PHP. It
-serves as the platform for Wikipedia and the other Wikimedia projects, used
-by hundreds of millions of people each month. MediaWiki is localised in over
-350 languages and its reliability and robust feature set have earned it a large
-and vibrant community of third-party users and developers.
-
-MediaWiki is:
-
-* feature-rich and extensible, both on-wiki and with hundreds of extensions;
-* scalable and suitable for both small and large sites;
-* simple to install, working on most hardware/software combinations; and
-* available in your language.
-
-For system requirements, installation, and upgrade details, see the files
-RELEASE-NOTES, INSTALL, and UPGRADE.
-
-* Ready to get started?
-** https://www.mediawiki.org/wiki/Special:MyLanguage/Download
-* Looking for the technical manual?
-** https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents
-* Seeking help from a person?
-** https://www.mediawiki.org/wiki/Special:MyLanguage/Communication
-* Looking to file a bug report or a feature request?
-** https://bugs.mediawiki.org/
-* Interested in helping out?
-** https://www.mediawiki.org/wiki/Special:MyLanguage/How_to_contribute
-
-MediaWiki is the result of global collaboration and cooperation. The CREDITS
-file lists technical contributors to the project. The COPYING file explains
-MediaWiki's copyright and license (GNU General Public License, version 2 or
-later). Many thanks to the Wikimedia community for testing and suggestions.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..ca703db
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+MediaWiki
+===========
+
+MediaWiki is a free and open-source wiki software package written in PHP. It
+serves as the platform for Wikipedia and the other Wikimedia projects, used
+by hundreds of millions of people each month. MediaWiki is localised in over
+350 languages and its reliability and robust feature set have earned it a large
+and vibrant community of third-party users and developers.
+
+MediaWiki is:
+
+* feature-rich and extensible, both on-wiki and with hundreds of extensions;
+* scalable and suitable for both small and large sites;
+* simple to install, working on most hardware/software combinations; and
+* available in your language.
+
+For system requirements, installation, and upgrade details, see the files
+RELEASE-NOTES, INSTALL, and UPGRADE.
+
+* Ready to get started?
+** https://www.mediawiki.org/wiki/Special:MyLanguage/Download
+* Looking for the technical manual?
+** https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents
+* Seeking help from a person?
+** https://www.mediawiki.org/wiki/Special:MyLanguage/Communication
+* Looking to file a bug report or a feature request?
+** https://bugs.mediawiki.org/
+* Interested in helping out?
+** https://www.mediawiki.org/wiki/Special:MyLanguage/How_to_contribute
+
+MediaWiki is the result of global collaboration and cooperation. The CREDITS
+file lists technical contributors to the project. The COPYING file explains
+MediaWiki's copyright and license (GNU General Public License, version 2 or
+later). Many thanks to the Wikimedia community for testing and suggestions.
diff --git a/README.mediawiki b/README.mediawiki
deleted file mode 120000 (symlink)
index 100b938..0000000
+++ /dev/null
@@ -1 +0,0 @@
-README
\ No newline at end of file
index 6aa832c..6e123a1 100644 (file)
@@ -225,6 +225,7 @@ $wgAutoloadLocalClasses = [
        'CapsCleanup' => __DIR__ . '/maintenance/cleanupCaps.php',
        'CategoriesRdf' => __DIR__ . '/includes/CategoriesRdf.php',
        'Category' => __DIR__ . '/includes/Category.php',
+       'CategoryChangesAsRdf' => __DIR__ . '/maintenance/categoryChangesAsRdf.php',
        'CategoryFinder' => __DIR__ . '/includes/CategoryFinder.php',
        'CategoryMembershipChange' => __DIR__ . '/includes/changes/CategoryMembershipChange.php',
        'CategoryMembershipChangeJob' => __DIR__ . '/includes/jobqueue/jobs/CategoryMembershipChangeJob.php',
index 8de4025..833e3bf 100644 (file)
@@ -1,6 +1,7 @@
 {
        "name": "mediawiki/core",
        "description": "Free software wiki application developed by the Wikimedia Foundation and others",
+       "type": "mediawiki-core",
        "keywords": ["mediawiki", "wiki"],
        "homepage": "https://www.mediawiki.org/",
        "authors": [
index 72375fb..87ca016 100644 (file)
@@ -7864,10 +7864,6 @@ $wgActionFilteredLogs = [
                'autocreate' => [ 'autocreate' ],
                'byemail' => [ 'byemail' ],
        ],
-       'patrol' => [
-               'patrol' => [ 'patrol' ],
-               'autopatrol' => [ 'autopatrol' ],
-       ],
        'protect' => [
                'protect' => [ 'protect' ],
                'modify' => [ 'modify' ],
index 097d510..d044ecb 100644 (file)
        "apihelp-query+allimages-param-sha1base36": "גיבוב SHA1 של התמונה בבסיס 36 (הבסיס בו נעשה שימוש במדיה־ויקי).",
        "apihelp-query+allimages-param-user": "להחזיר רק קבצים שהועלו על־ידי המשתמש הזה. יכול לשמש רק עם $1sort=timestamp. לא יכול לשמש יחד עם $1filterbots.",
        "apihelp-query+allimages-param-filterbots": "איך לסנן קבצים שמעלים בוטים. יכול לשמש רק עם $1sort=timestamp. לא יכול לשמש יחד עם $1user.",
-       "apihelp-query+allimages-param-mime": "×\90×\99×\9c×\95 ×¡×\95×\92×\99 MIME ×\9c×\97×\91פש, ×\9c×\9eש×\9c <kbd>image/jpeg</kbd>.",
+       "apihelp-query+allimages-param-mime": "אילו סוגי MIME לחפש, למשל <kbd>image/jpeg</kbd>.",
        "apihelp-query+allimages-param-limit": "כמה תמונות להחזיר בסך הכול.",
        "apihelp-query+allimages-example-B": "הצגת רשימה של קבצים שמתחילים באות <kbd>B</kbd>.",
        "apihelp-query+allimages-example-recent": "הצגת רשימת קבצים שהועלו לאחרונה, דומה ל־[[Special:NewFiles]].",
        "apihelp-query+categories-paramvalue-prop-hidden": "תיוג קטגוריות שהוסתרו באמצעות <code>_&#95;HIDDENCAT_&#95;</code>.",
        "apihelp-query+categories-param-show": "איזה סוג של קטגוריות להציג.",
        "apihelp-query+categories-param-limit": "כמה קטגוריות להחזיר.",
-       "apihelp-query+categories-param-categories": "×\9cרש×\95×\9d ×¨×§ ×\90ת ×\94ק×\98×\92×\95ר×\99×\95ת ×\94×\90×\9c×\95. ×©×\99×\9e×\95ש×\99 ×\9c×\91×\93×\99ק×\94 ×¢ם דף מסוים נמצא בקטגוריה מסוימת.",
+       "apihelp-query+categories-param-categories": "×\9cרש×\95×\9d ×¨×§ ×\90ת ×\94ק×\98×\92×\95ר×\99×\95ת ×\94×\90×\9c×\95. ×©×\99×\9e×\95ש×\99 ×\9c×\91×\93×\99ק×\94 ×\90ם דף מסוים נמצא בקטגוריה מסוימת.",
        "apihelp-query+categories-param-dir": "באיזה כיוון לרשום.",
        "apihelp-query+categories-example-simple": "קבלת רשימת קטגוריות שהם <kbd>Albert Einstein</kbd> שייך אליהן.",
        "apihelp-query+categories-example-generator": "קבלת מידע על כל הקטגוריות שמשמשות בדף <kbd>Albert Einstein</kbd>.",
        "apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "משמש את Special:Upload כדי לקבל מידע על קובץ קיים. לא נועד לשימוש מחוץ לליבת MediaWiki.",
        "apihelp-query+imageinfo-paramvalue-prop-badfile": "מוסיף האם הקובץ נמצא ב־[[MediaWiki:Bad image list]]",
        "apihelp-query+imageinfo-param-limit": "כמה גרסאות של קובץ לכל קובץ.",
-       "apihelp-query+imageinfo-param-start": "×\9e×\90×\99×\96 ×\97×\95ת×\9dÖ¾×\96×\9e×\9f ×\9c×\94ת×\97×\99×\9c רשימה.",
+       "apihelp-query+imageinfo-param-start": "×\9e×\90×\99×\9c×\95 ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9c×\94ת×\97×\99×\9c ×\90ת ×\94רשימה.",
        "apihelp-query+imageinfo-param-end": "באיזה חותם־זמן לסיים את הרשימה.",
        "apihelp-query+imageinfo-param-urlwidth": "אם מוגדר $2prop=url, יוחזר URL לתמונה שגודלה הותאם לרוחב הזה.\nמסיבות של ביצועים, אם האפשרות הזאת משמשת, לא יוחזרו יותר מ־$1 תמונות.",
        "apihelp-query+imageinfo-param-urlheight": "דומה ל־$1urlwidth.",
        "apihelp-query+recentchanges-param-end": "באיזה חותם זמן להפסיק לרשום.",
        "apihelp-query+recentchanges-param-namespace": "לסנן את השינויים רק למרחבי השם האלה.",
        "apihelp-query+recentchanges-param-user": "לרשום רק שינויים של המשתמש הזה.",
-       "apihelp-query+recentchanges-param-excludeuser": "Don't list changes by this user",
+       "apihelp-query+recentchanges-param-excludeuser": "לא לרשום שינויים ממשתמש זה.",
        "apihelp-query+recentchanges-param-tag": "לרשום רק שינויים שמתויגים עם התג הזה.",
        "apihelp-query+recentchanges-param-prop": "לכלול פריטי מידע נוספים:",
        "apihelp-query+recentchanges-paramvalue-prop-user": "הוספת המשתמש האחראי על העריכה ותיוג אם זאת כתובת IP.",
        "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "רשימת כינויי מרחבי שם רשומים.",
        "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "רשימת כינויים דפים מיוחדים.",
        "apihelp-query+siteinfo-paramvalue-prop-magicwords": "רשימות מילות קסם וכינוייהן.",
-       "apihelp-query+siteinfo-paramvalue-prop-statistics": "×\94×\97×\96ר×\96ת ×¡×\98×\98×\99ס×\98×\99ק×\95ת אתר.",
+       "apihelp-query+siteinfo-paramvalue-prop-statistics": "×\94×\97×\96רת ×¡×\98×\98×\99ס×\98×\99ק×\95ת ×©×\9c ×\94אתר.",
        "apihelp-query+siteinfo-paramvalue-prop-interwikimap": "החזרת מפת בינוויקי (אפשר שתהיה מסוננת, אפשר שתהיה מותאמת מקומית באמצעות <var>$1inlanguagecode</var>).",
        "apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "החזרת שרת מסד־נתונים עם שיהוי השכפול הגבוה ביותר.",
        "apihelp-query+siteinfo-paramvalue-prop-usergroups": "החזרת קבוצות משתמשים וההרשאות המשויכות.",
        "apihelp-query+watchlist-param-end": "באיזה חותם זמן להפסיק לרשום.",
        "apihelp-query+watchlist-param-namespace": "סינון שינויים רק למרחבי השם שניתנו.",
        "apihelp-query+watchlist-param-user": "לרשום רק שינויים של המשתמש הזה.",
-       "apihelp-query+watchlist-param-excludeuser": "Don't list changes by this user",
+       "apihelp-query+watchlist-param-excludeuser": "לא לרשום שינויים ממשתמש זה.",
        "apihelp-query+watchlist-param-limit": "כמה תוצאות סך הכול להחזיר בכל בקשה.",
        "apihelp-query+watchlist-param-prop": "אילו מאפיינים נוספים לקבל:",
        "apihelp-query+watchlist-paramvalue-prop-ids": "הוספת מזהי גסה ומזהי דף.",
        "apihelp-rollback-param-title": "שם הדף לשחזור. לא יכול לשמש יחד עם <var>$1pageid</var>.",
        "apihelp-rollback-param-pageid": "מזהה הדף לשחזור. לא יכול לשמש יחד עם <var>$1title</var>.",
        "apihelp-rollback-param-tags": "אילו תגים להחיל על השחזור.",
-       "apihelp-rollback-param-user": "שם המשתמשים שהעריכות שלו תשוחזרנה.",
+       "apihelp-rollback-param-user": "שם המשתמש שהעריכות שלו תשוחזרנה.",
        "apihelp-rollback-param-summary": "תקציר עריכה מותאם. אם ריק, ישמש תקציר לפי בררת מחדל.",
        "apihelp-rollback-param-markbot": "לסמן את העריכות ששוחזרו ואת השחזור בתור עריכות בוט.",
        "apihelp-rollback-param-watchlist": "הוספה או הסרה של הדף ללא תנאי מרשימת המעקב של המשתמש הנוכחי, להשתמש בהעדפות או לא לשנות את המעקב.",
        "apihelp-setnotificationtimestamp-example-page": "אתחול מצב ההודעה עבור <kbd>Main Page</kbd>.",
        "apihelp-setnotificationtimestamp-example-pagetimestamp": "הגדרת חותם־הזמן להודעה ל־<kbd>Main page</kbd> כך שכל העריכות מאז 1 בינואר 2012 מוגדרות בתור כאלה שלא נצפו.",
        "apihelp-setnotificationtimestamp-example-allpages": "אתחול מצב ההודעה עבור דפים במרחב השם <kbd>{{ns:user}}</kbd>.",
-       "apihelp-setpagelanguage-summary": "שנ×\94 ×\90ת ×\94שפ×\94 ×©×\9c ×\93×£",
-       "apihelp-setpagelanguage-extended-description-disabled": "ש×\99× ×\95×\99 ×\94שפ×\94 ×©×\9c ×\93×£ ×\9c×\90 ×\9e×\95רש×\94 ×\91×\95×\95×\99ק×\99 ×\96×\94.\n\n×\94פע×\9c ×\90ת <var>[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> ×¢×\9c ×\9eנת ×\9c×\94שת×\9eש ×\91פע×\95×\9c×\94 ×\96×\95",
+       "apihelp-setpagelanguage-summary": "ש×\99× ×\95×\99 ×\94שפ×\94 ×©×\9c ×\93×£.",
+       "apihelp-setpagelanguage-extended-description-disabled": "×\9c×\90 × ×\99ת×\9f ×\9cשנ×\95ת ×©×¤×\95ת ×©×\9c ×\93פ×\99×\9d ×\91×\90תר ×\94×\95×\95×\99ק×\99 ×\94×\96×\94.\n\n×\99ש ×\9c×\94פע×\99×\9c ×\90ת <var dir=\"ltr\">[[mw:Special:MyLanguage/Manual:$wgPageLanguageUseDB|$wgPageLanguageUseDB]]</var> ×¢×\9cÖ¾×\9eנת ×\9c×\94שת×\9eש ×\91פע×\95×\9c×\94 ×\96×\95.",
        "apihelp-setpagelanguage-param-title": "כותרת הדף שאת שפתו ברצונך לשנות. לא אפשרי להשתמש באפשרות עם <var>$1pageid</var>.",
        "apihelp-setpagelanguage-param-pageid": "מזהה הדף שאת שפתו ברצונך לשנות. לא אפשרי להשתמש באפשרות עם <var>$1title</var>.",
        "apihelp-setpagelanguage-param-lang": "קוד השפה של השפה שאליה צריך לשנות את הדף. יש להשתמש ב־<kbd>default</kbd> כדי לאתחל את הדף לשפת בררת המחדל של הוויקי.",
        "apihelp-phpfm-summary": "לפלוט נתונים בתסדיר PHP מוסדר (עם הדפסה יפה ב־HTML).",
        "apihelp-rawfm-summary": "לפלוט את הנתונים, כולל אלמנטים לניפוי שגיאות, בתסדיר JSON (עם הדפסה יפה ב־HTML).",
        "apihelp-xml-summary": "לפלוט נתונים בתסדיר XML.",
-       "apihelp-xml-param-xslt": "×\90×\9d ×¦×\95×\99×\9f, ×\99ש ×\9c×\94×\95ס×\99×£ ×\90ת ×©×\9d ×\94×\93×£ ×\9b×\92×\99×\9c×\99×\95×\9f ×¢×\99צ×\95×\91 XSL. ×¢×\9c ×\94ער×\9a ×\9c×\94×\99×\95ת ×\9b×\95תרת ×\91 {{ns:MediaWiki}} ×\91×\9eר×\97×\91 ×©×\9d ×\94×\9eשת×\9eש, ×\94×\9eסת×\99×\99×\9d ×\91-  <code>.xsl</code>.",
+       "apihelp-xml-param-xslt": "×\90×\9d ×¦×\95×\99×\9f, ×\94×\93×£ ×\99ת×\95×\95סף ×\9b×\92×\99×\9c×\99×\95×\9f XSL. ×\94ער×\9a ×\97×\99×\99×\91 ×\9c×\94×\99×\95ת ×\9b×\95תרת ×\91×\9eר×\97×\91 ×\94ש×\9d \"{{ns:MediaWiki}}\" ×©×\9eסת×\99×\99×\9eת ×\91Ö¾<code dir=\"ltr\">.xsl</code>.",
        "apihelp-xml-param-includexmlnamespace": "אם זה צוין, מוסיף מרחב שם של XML.",
        "apihelp-xmlfm-summary": "לפלוט נתונים בתסדיר XML (עם הדפסה יפה ב־HTML).",
        "api-format-title": "תוצאה של API של מדיה־ויקי",
        "apierror-writeapidenied": "אין לך הרשאה לערוך את הוויקי הזה דרך ה־API.",
        "apiwarn-alldeletedrevisions-performance": "לביצועים טובים יותר בעת יצירת כותרת, יש להשתמש ב־<kbd>$1dir=newer</kbd>.",
        "apiwarn-badurlparam": "לא היה אפשר לפענח את <var>$1urlparam</var> עבור $2. משתמשים רק ב־width ו־height.",
-       "apiwarn-badutf8": "×\94ער×\9a ×\94ער×\9a ×©×\94×\95×¢×\91ר ×\9cÖ¾<var>$1</var> ×\9e×\9b×\99×\9c × ×ª×\95× ×\99×\9d ×\91×\9cת×\99־תק×\99× ×\99×\9d ×\90×\95 ×\91×\9cת×\99Ö¾×\9e× ×\95ר×\9e×\9c×\99×\9d. × ×ª×\95× ×\99×\9d ×\98קס×\98 ×\90×\9e×\95ר×\99×\9d ×\9c×\94×\99×\95ת ×ª×§×\99× ×\99×\9d, ×\9e× ×\95ר×\9e×\9c×\99 NFC ×\9c×\9c×\90 ×ª×\95×\95×\99 ×\91קר×\94 C0 ×\9c×\9e×¢×\98 HT (\\t)â\80\8f, LF (\\n), ×\95Ö¾CR (\\r).",
+       "apiwarn-badutf8": "×\94ער×\9a ×©×\94×\95×¢×\91ר ×\9cÖ¾<var>$1</var> ×\9e×\9b×\99×\9c × ×ª×\95× ×\99×\9d ×\91×\9cת×\99־תק×\99× ×\99×\9d ×\90×\95 ×\91×\9cת×\99Ö¾×\9e× ×\95ר×\9e×\9c×\99×\9d. × ×ª×\95× ×\99×\9d ×\98קס×\98 ×\90×\9e×\95ר×\99×\9d ×\9c×\94×\99×\95ת ×ª×§×\99× ×\99×\9d, ×\9e× ×\95ר×\9e×\9c×\99 NFC ×\95×\9c×\9c×\90 ×ª×\95×\95×\99 ×\91קר×\94 C0 ×\9c×\9e×¢×\98 <span dir=\"ltr\">HT (\\t)</span>&rlm;, <span dir=\"ltr\">LF (\\n)</span>&rlm; ×\95Ö¾<span dir=\"ltr\">CR (\\r)</span>.",
        "apiwarn-checktoken-percentencoding": "נא לבדוק שסימנים כמו \"+\" באסימון מקודדים עם אחוזים בצורה נכונה ב־URL.",
        "apiwarn-compare-nocontentmodel": "לא היה אפשר לקבוע את מודל התוכן, נניח שזה $1.",
        "apiwarn-deprecation-deletedrevs": "<kbd>list=deletedrevs</kbd> הוצהר בתור מיושן. נא להשתמש ב־ <kbd>prop=deletedrevisions</kbd> או ב־<kbd>list=alldeletedrevisions</kbd> במקום זה.",
        "apiwarn-errorprinterfailed-ex": "מדפיס השגיאות לא עבד (ינסה שוב ללא פרמטרים): $1",
        "apiwarn-invalidcategory": "\"$1\" אינה קטגוריה.",
        "apiwarn-invalidtitle": "\"$1\" אינה כותרת תקינה.",
-       "apiwarn-invalidxmlstylesheetext": "לגיליון הסגנונות אמור להיות הסיומת <code dir=\"ltr\">.xsl</code>.",
+       "apiwarn-invalidxmlstylesheetext": "לגיליון הסגנונות אמורה להיות הסיומת <code dir=\"ltr\">.xsl</code>.",
        "apiwarn-invalidxmlstylesheet": "ניתן גיליון סגנונות שאינו תקין או אינו קיים.",
        "apiwarn-invalidxmlstylesheetns": "גיליון הסגנונות אמור להיות במרחב השם {{ns:MediaWiki}}.",
        "apiwarn-moduleswithoutvars": "המאפיין <kbd>modules</kbd> לא הוגדר, אבל לא <kbd>jsconfigvars</kbd> או <kbd>encodedjsconfigvars</kbd>. משתני הגדרות נחוצים בשביל שימוש נכון במודולים.",
        "apiwarn-toomanyvalues": "יותר מדי ערכים סופקו לפרמטר <var>$1</var>. המגבלה היא $2.",
        "apiwarn-truncatedresult": "התוצאה נחתכה כי אחרת היא הייתה ארוכה מהמגבלה של $1 בתים.",
        "apiwarn-unclearnowtimestamp": "העברת \"$2\" בתור פרמטר חותם־זמן <var>$1</var> הוצהרה בתור מיושנת. אם מסיבה כלשהי אתם צריכים להגדיר במפורש את הזמן הנוכחי ללא חישובו בצד הלקוח, יש להשתמש ב־<kbd>now</kbd>.",
-       "apiwarn-unrecognizedvalues": "לפרמטר <var>$1</var> היתנ ג{{PLURAL:$3|ניתן ערך בלתי־ידוע|ניתנו ערכים בלתי־ידועים}}: $2.",
+       "apiwarn-unrecognizedvalues": "לפרמטר <var>$1</var> {{PLURAL:$3|ניתן ערך בלתי־ידוע|ניתנו ערכים בלתי־ידועים}}: $2.",
        "apiwarn-unsupportedarray": "הפרמטר <var>$1</var> משתמש בתחביר מערכים שאינו נתמך ב־PHP.",
        "apiwarn-urlparamwidth": "התעלמות מרוחב (width) שהוגדר ב־<var>$1urlparam</var> (ערך: $2) לטובת רוחב שנגזר מ־<var>$1urlwidth</var>/<var>$1urlheight</var> (ערך: $3).",
        "apiwarn-validationfailed-badchars": "תווים בלתי־תקינים במפתח (מותרים רק <code>a-z</code>‏, <code>A-Z</code>‏, <code>0-9</code>‏, <code>_</code>, ו־<code>-</code>).",
index 36efdb3..9ac81ae 100644 (file)
@@ -384,9 +384,17 @@ class IcuCollation extends Collation {
                foreach ( $letters as $letter ) {
                        $key = $this->getPrimarySortKey( $letter );
                        if ( isset( $letterMap[$key] ) ) {
-                               // Primary collision
-                               // Keep whichever one sorts first in the main collator
-                               if ( $this->mainCollator->compare( $letter, $letterMap[$key] ) < 0 ) {
+                               // Primary collision (two characters with the same sort position).
+                               // Keep whichever one sorts first in the main collator.
+                               $comp = $this->mainCollator->compare( $letter, $letterMap[$key] );
+                               wfDebug( "Primary collision '$letter' '{$letterMap[$key]}' (comparison: $comp)\n" );
+                               // If that also has a collision, use codepoint as a tiebreaker.
+                               if ( $comp === 0 ) {
+                                       // TODO Use <=> operator when PHP 7 is allowed.
+                                       $comp = UtfNormal\Utils::utf8ToCodepoint( $letter ) -
+                                               UtfNormal\Utils::utf8ToCodepoint( $letterMap[$key] );
+                               }
+                               if ( $comp < 0 ) {
                                        $letterMap[$key] = $letter;
                                }
                        } else {
index 38c7a5c..955c28d 100644 (file)
@@ -95,6 +95,8 @@ abstract class LBFactory implements ILBFactory {
        const ROUND_BEGINNING = 'within-begin';
        const ROUND_COMMITTING = 'within-commit';
        const ROUND_ROLLING_BACK = 'within-rollback';
+       const ROUND_COMMIT_CALLBACKS = 'within-commit-callbacks';
+       const ROUND_ROLLBACK_CALLBACKS = 'within-rollback-callbacks';
 
        private static $loggerFields =
                [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
@@ -170,28 +172,28 @@ abstract class LBFactory implements ILBFactory {
        /**
         * @see ILBFactory::newMainLB()
         * @param bool $domain
-        * @return LoadBalancer
+        * @return ILoadBalancer
         */
        abstract public function newMainLB( $domain = false );
 
        /**
         * @see ILBFactory::getMainLB()
         * @param bool $domain
-        * @return LoadBalancer
+        * @return ILoadBalancer
         */
        abstract public function getMainLB( $domain = false );
 
        /**
         * @see ILBFactory::newExternalLB()
         * @param string $cluster
-        * @return LoadBalancer
+        * @return ILoadBalancer
         */
        abstract public function newExternalLB( $cluster );
 
        /**
         * @see ILBFactory::getExternalLB()
         * @param string $cluster
-        * @return LoadBalancer
+        * @return ILoadBalancer
         */
        abstract public function getExternalLB( $cluster );
 
@@ -261,6 +263,7 @@ abstract class LBFactory implements ILBFactory {
                // Actually perform the commit on all master DB connections and revert DBO_TRX
                $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
                // Run all post-commit callbacks in a separate step
+               $this->trxRoundStage = self::ROUND_COMMIT_CALLBACKS;
                $e = $this->executePostTransactionCallbacks();
                $this->trxRoundStage = self::ROUND_CURSORY;
                // Throw any last post-commit callback error
@@ -275,6 +278,7 @@ abstract class LBFactory implements ILBFactory {
                // Actually perform the rollback on all master DB connections and revert DBO_TRX
                $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
                // Run all post-commit callbacks in a separate step
+               $this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
                $this->executePostTransactionCallbacks();
                $this->trxRoundStage = self::ROUND_CURSORY;
        }
@@ -547,10 +551,18 @@ abstract class LBFactory implements ILBFactory {
        }
 
        /**
-        * Base parameters to LoadBalancer::__construct()
+        * Base parameters to ILoadBalancer::__construct()
         * @return array
         */
        final protected function baseLoadBalancerParams() {
+               if ( $this->trxRoundStage === self::ROUND_COMMIT_CALLBACKS ) {
+                       $initStage = ILoadBalancer::STAGE_POSTCOMMIT_CALLBACKS;
+               } elseif ( $this->trxRoundStage === self::ROUND_ROLLBACK_CALLBACKS ) {
+                       $initStage = ILoadBalancer::STAGE_POSTROLLBACK_CALLBACKS;
+               } else {
+                       $initStage = null;
+               }
+
                return [
                        'localDomain' => $this->localDomain,
                        'readOnlyReason' => $this->readOnlyReason,
@@ -570,7 +582,8 @@ abstract class LBFactory implements ILBFactory {
                                // Defer ChronologyProtector construction in case setRequestInfo() ends up
                                // being called later (but before the first connection attempt) (T192611)
                                $this->getChronologyProtector()->initLB( $lb );
-                       }
+                       },
+                       'roundStage' => $initStage
                ];
        }
 
index 850f9af..81ce4ba 100644 (file)
@@ -89,6 +89,11 @@ interface ILoadBalancer {
        /** @var int Alias for CONN_TRX_AUTOCOMMIT for b/c; deprecated since 1.31 */
        const CONN_TRX_AUTO = 1;
 
+       /** @var string Manager of ILoadBalancer instances is running post-commit callbacks */
+       const STAGE_POSTCOMMIT_CALLBACKS = 'stage-postcommit-callbacks';
+       /** @var string Manager of ILoadBalancer instances is running post-rollback callbacks */
+       const STAGE_POSTROLLBACK_CALLBACKS = 'stage-postrollback-callbacks';
+
        /**
         * Construct a manager of IDatabase connection objects
         *
@@ -112,6 +117,7 @@ interface ILoadBalancer {
         *  - perfLogger: PSR-3 logger instance. [optional]
         *  - errorLogger : Callback that takes an Exception and logs it. [optional]
         *  - deprecationLogger: Callback to log a deprecation warning. [optional]
+        *  - roundStage: STAGE_POSTCOMMIT_* class constant; for internal use [optional]
         * @throws InvalidArgumentException
         */
        public function __construct( array $params );
index 405ed14..360be42 100644 (file)
@@ -261,6 +261,14 @@ class LoadBalancer implements ILoadBalancer {
                if ( isset( $params['chronologyCallback'] ) ) {
                        $this->chronologyCallback = $params['chronologyCallback'];
                }
+
+               if ( isset( $params['roundStage'] ) ) {
+                       if ( $params['roundStage'] === self::STAGE_POSTCOMMIT_CALLBACKS ) {
+                               $this->trxRoundStage = self::ROUND_COMMIT_CALLBACKS;
+                       } elseif ( $params['roundStage'] === self::STAGE_POSTROLLBACK_CALLBACKS ) {
+                               $this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
+                       }
+               }
        }
 
        /**
index 93a81cf..9e4a630 100644 (file)
@@ -97,7 +97,7 @@ class LogEventsList extends ContextSource {
         * @param array|string $types
         * @param string $user
         * @param string $page
-        * @param string $pattern
+        * @param bool $pattern
         * @param int|string $year Use 0 to start with no year preselected.
         * @param int|string $month A month in the 1..12 range. Use 0 to start with no month
         *  preselected.
@@ -105,7 +105,7 @@ class LogEventsList extends ContextSource {
         * @param string $tagFilter Tag to select by default
         * @param string $action
         */
-       public function showOptions( $types = [], $user = '', $page = '', $pattern = '', $year = 0,
+       public function showOptions( $types = [], $user = '', $page = '', $pattern = false, $year = 0,
                $month = 0, $filter = null, $tagFilter = '', $action = null
        ) {
                global $wgScript, $wgMiserMode;
@@ -289,7 +289,7 @@ class LogEventsList extends ContextSource {
        }
 
        /**
-        * @param string $pattern
+        * @param bool $pattern
         * @return string Checkbox
         */
        private function getTitlePattern( $pattern ) {
index c047e96..84653b1 100644 (file)
@@ -36,8 +36,8 @@ class LogPager extends ReverseChronologicalPager {
        /** @var string|Title Events limited to those about Title when set */
        private $title = '';
 
-       /** @var string */
-       private $pattern = '';
+       /** @var bool */
+       private $pattern = false;
 
        /** @var string */
        private $typeCGI = '';
@@ -59,7 +59,7 @@ class LogPager extends ReverseChronologicalPager {
         * @param string|array $types Log types to show
         * @param string $performer The user who made the log entries
         * @param string|Title $title The page title the log entries are for
-        * @param string $pattern Do a prefix search rather than an exact title match
+        * @param bool $pattern Do a prefix search rather than an exact title match
         * @param array $conds Extra conditions for the query
         * @param int|bool $year The year to start from. Default: false
         * @param int|bool $month The month to start from. Default: false
@@ -68,7 +68,7 @@ class LogPager extends ReverseChronologicalPager {
         * @param int $logId Log entry ID, to limit to a single log entry.
         */
        public function __construct( $list, $types = [], $performer = '', $title = '',
-               $pattern = '', $conds = [], $year = false, $month = false, $tagFilter = '',
+               $pattern = false, $conds = [], $year = false, $month = false, $tagFilter = '',
                $action = '', $logId = false
        ) {
                parent::__construct( $list->getContext() );
@@ -194,7 +194,7 @@ class LogPager extends ReverseChronologicalPager {
         * (For the block and rights logs, this is a user page.)
         *
         * @param string|Title $page Title name
-        * @param string $pattern
+        * @param bool $pattern
         * @return void
         */
        private function limitTitle( $page, $pattern ) {
@@ -398,6 +398,9 @@ class LogPager extends ReverseChronologicalPager {
                return $this->title;
        }
 
+       /**
+        * @return bool
+        */
        public function getPattern() {
                return $this->pattern;
        }
index 7e6e8e6..0f65711 100644 (file)
@@ -335,12 +335,25 @@ abstract class SearchEngine {
                        return false;
                }
                $extractedNamespace = null;
+               $allkeywords = [];
 
-               $allkeyword = wfMessage( 'searchall' )->inContentLanguage()->text() . ":";
-               if ( strncmp( $query, $allkeyword, strlen( $allkeyword ) ) == 0 ) {
-                       $extractedNamespace = null;
-                       $parsed = substr( $query, strlen( $allkeyword ) );
-               } elseif ( strpos( $query, ':' ) !== false ) {
+               $allkeywords[] = wfMessage( 'searchall' )->inContentLanguage()->text() . ":";
+               // force all: so that we have a common syntax for all the wikis
+               if ( !in_array( 'all:', $allkeywords ) ) {
+                       $allkeywords[] = 'all:';
+               }
+
+               $allQuery = false;
+               foreach ( $allkeywords as $kw ) {
+                       if ( strncmp( $query, $kw, strlen( $kw ) ) == 0 ) {
+                               $extractedNamespace = null;
+                               $parsed = substr( $query, strlen( $kw ) );
+                               $allQuery = true;
+                               break;
+                       }
+               }
+
+               if ( !$allQuery && strpos( $query, ':' ) !== false ) {
                        // TODO: should we unify with PrefixSearch::extractNamespace ?
                        $prefix = str_replace( ' ', '_', substr( $query, 0, strpos( $query, ':' ) ) );
                        $index = $wgContLang->getNsIndex( $prefix );
index f67fe9f..1cfcffa 100644 (file)
@@ -36,8 +36,6 @@ class SpecialPreferences extends SpecialPage {
 
        function __construct() {
                parent::__construct( 'Preferences' );
-
-               $this->oouiEnabled = self::isOouiEnabled( $this->getContext() );
        }
 
        /**
@@ -56,6 +54,8 @@ class SpecialPreferences extends SpecialPage {
        }
 
        public function execute( $par ) {
+               $this->oouiEnabled = static::isOouiEnabled( $this->getContext() );
+
                $this->setHeaders();
                $this->outputHeader();
                $out = $this->getOutput();
index 81b6d0f..e118a0c 100644 (file)
@@ -25,7 +25,7 @@
                ]
        },
        "tog-underline": "সংযোগসমূহ অধোৰেখিত কৰক:",
-       "tog-hideminor": "সামà§\8dপà§\8dৰতিà¦\95 সাল-সলনিত অগুৰুত্বপূৰ্ণ সম্পাদনা নেদেখুৱাব",
+       "tog-hideminor": "শà§\87হতà§\80য়া সাল-সলনিত অগুৰুত্বপূৰ্ণ সম্পাদনা নেদেখুৱাব",
        "tog-hidepatrolled": "সাম্প্ৰতিক সাল-সলনিত তহলদাৰী সম্পাদনা নেদেখুৱাব",
        "tog-newpageshidepatrolled": "নতুন পৃষ্ঠা তালিকাত তহলদাৰী পৃষ্ঠাসমূহ নেদেখুৱাব",
        "tog-hidecategorization": "পৃষ্ঠাবোৰৰ শ্ৰেণীকৰণ লুকুৱাওক",
        "recentchanges-legend-plusminus": "(''±১২৩'')",
        "rcfilters-legend-heading": "<strong>সংক্ষিপ্ত ৰূপৰ তালিকা:</strong>",
        "rcfilters-other-review-tools": "আন পুনৰীক্ষণ সঁজুলি",
+       "rcfilters-filter-humans-label": "মানুহ (ব'ট নহয়)",
        "rcnotefrom": "<strong>$2</strong>ৰ পৰা হোৱা পৰিৱৰ্তনসমূহ (সৰ্বোচ্চ <strong>$1টা</strong> দেখুৱা হৈছে)।",
        "rclistfrom": "$3 $2ৰ পৰা নতুন সালসলনি দেখুৱাওক",
        "rcshowhideminor": "$1 -সংখ্যক নগণ্য সম্পাদনা",
index f7e2265..d6fb716 100644 (file)
        "recentchangeslinked-feed": "باغلی دَییشیکلیک‌لر",
        "recentchangeslinked-toolbox": "باغلی دَییشیکلیک‌لر",
        "recentchangeslinked-title": "''$1'' ایله باغلی دییشیکلر",
-       "recentchangeslinked-summary": "آشاغیداکی سیياهی، قئيد اوْلونان صحیفه‌‌يه (و يا قئيد اوْلونان کاتئقوْرياداکی صحیفه‌‌لره) داخیلی کئچید وئرن صحیفه‌‌لرده ائدیلمیش سوْن ديَیشیکلیکلرین سیياهیسیدیر. \n[[Special:Watchlist|ایزله‌مه سیياهینیزداکی]] صحیفه‌‌لر '''قالین''' شریفتله گؤستریلمیشدیر.",
+       "recentchangeslinked-summary": "آشاغیداکی ژورنال، قئيد اوْلونان صفحه‌‌يه (و يا قئيد اوْلونان بؤلمه‌ده‌کی صفحه‌‌لره) داخیلی کئچید وئرن صفحه‌‌لرده ائدیلمیش سوْن ديَیشیکلیکلرین لیستیدیر. \n[[Special:Watchlist|ایزله‌مه لیستینیزده]]‌کی صفحه‌‌لر '''قالین''' شریفتله گؤستریلمیشدیر.",
        "recentchangeslinked-page": "صفحه آدی:",
        "recentchangeslinked-to": "قئيد اوْلونان صحیفه‌‌ده‌کی دئيیل، اوْنا داخیلی کئچید وئرن صحیفه‌‌لرده‌کی ديَیشیکلیکلری گؤستر",
        "upload": "فایل یوکله‌",
index df3cd07..4365fb4 100644 (file)
        "botpasswords-restriction-failed": "Уваход ня выкананы праз абмежаваньні на пароль робата",
        "botpasswords-invalid-name": "Пададзенае імя ўдзельніка ня ўтрымлівае падзяляльнік для паролю робата («$1»).",
        "botpasswords-not-exist": "Удзельнік «$1» ня мае паролю для робата з назвай «$2».",
+       "botpasswords-needs-reset": "Пароль для робата зь імем «$2» {{GENDER:$1|удзельніка|удзельніцы}} «$1» мусіць быць скінуты.",
        "resetpass_forbidden": "Пароль ня можа быць зьменены",
        "resetpass_forbidden-reason": "Паролі ня могуць быць зьмененыя: $1",
        "resetpass-no-info": "Для непасрэднага доступу да гэтай старонкі Вам неабходна ўвайсьці ў сыстэму.",
        "subject-preview": "Папярэдні прагляд загалоўку:",
        "previewerrortext": "Адбылася памылка пры спробе папярэдняга прагляду вашых зьменаў.",
        "blockedtitle": "Удзельнік заблякаваны",
-       "blockedtext": "<strong>Ð\92аÑ\88 Ñ\80аÑ\85Ñ\83нак Ñ\83дзелÑ\8cнÑ\96ка Ñ\86Ñ\96 IP-адÑ\80аÑ\81 Ð±Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b.</strong>\n\nÐ\91лÑ\8fкаванÑ\8cне Ð²Ñ\8bканаÑ\9e $1.\nÐ\9fÑ\80Ñ\8bÑ\87Ñ\8bна Ð³Ñ\8dÑ\82ага: <em>$2</em>.\n\n* Ð\9fаÑ\87аÑ\82ак Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f: $8\n* Ð¡ÐºÐ°Ð½Ñ\87Ñ\8dнÑ\8cне Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f: $6\n* Ð\91Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b: $7\n\nÐ\92Ñ\8b Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\81канÑ\82акÑ\82аваÑ\86Ñ\86а Ð· $1 Ñ\86Ñ\96 Ð°Ð´Ð½Ñ\8bм Ð·Ñ\8c Ñ\96нÑ\88Ñ\8bÑ\85 [[{{MediaWiki:Grouppage-sysop}}|адмÑ\96нÑ\96Ñ\81Ñ\82Ñ\80аÑ\82аÑ\80аÑ\9e]], ÐºÐ°Ð± Ð°Ð±Ð¼ÐµÑ\80каваÑ\86Ñ\8c Ð±Ð»Ñ\8fкаванÑ\8cне. Ð\97аÑ\9eважÑ\86е, Ñ\88Ñ\82о Ð\92Ñ\8b Ð½Ñ\8f Ð·Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\9eжÑ\8bÑ\86Ñ\8c Ð¼Ð°Ð³Ñ\87Ñ\8bмаÑ\81Ñ\8cÑ\86Ñ\8c Â«Ð´Ð°Ñ\81лаÑ\86Ñ\8c Ð»Ñ\96Ñ\81Ñ\82 Ð¿Ð° Ñ\8dлекÑ\82Ñ\80оннай Ð¿Ð¾Ñ\88Ñ\86е», Ð¿Ð°ÐºÑ\83лÑ\8c Ð½Ðµ Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bÑ\86е Ñ\81апÑ\80аÑ\9eднÑ\8b Ð°Ð´Ñ\80аÑ\81 Ñ\8dлекÑ\82Ñ\80оннай Ð¿Ð¾Ñ\88Ñ\82Ñ\8b Ñ\9e Ð\92аÑ\88Ñ\8bÑ\85 [[Special:Preferences|наладаÑ\85]], Ñ\96 ÐºÐ°Ð»Ñ\96 Ð³Ñ\8dÑ\82а Ð\92ам Ð½Ðµ Ð±Ñ\8bло Ð·Ð°Ð±Ð°Ñ\80онена.\nÐ\92аÑ\88 IP-адÑ\80аÑ\81 â\80\94 $3, Ñ\96дÑ\8dнÑ\82Ñ\8bÑ\84Ñ\96каÑ\82аÑ\80 Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f â\80\94 #$5.\nÐ\9aалÑ\96 Ð»Ð°Ñ\81ка, Ñ\83лÑ\83Ñ\87айÑ\86е Ñ\9eÑ\81Ñ\8e Ð²Ñ\8bÑ\88Ñ\8dйпададзенÑ\83Ñ\8e Ñ\96нÑ\84аÑ\80маÑ\86Ñ\8bÑ\8e Ð²Ð° Ñ\9eÑ\81е Ð·Ð°Ð¿Ñ\8bÑ\82Ñ\8b, Ñ\88Ñ\82о Ð\92ы будзеце рабіць.",
-       "autoblockedtext": "Ð\92аÑ\88 IP-адÑ\80аÑ\81 Ð±Ñ\8bÑ\9e Ð°Ñ\9eÑ\82амаÑ\82Ñ\8bÑ\87на Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b, Ñ\82амÑ\83 Ñ\88Ñ\82о Ñ\91н Ñ\83жÑ\8bваÑ\9eÑ\81Ñ\8f Ñ\96нÑ\88Ñ\8bм Ñ\83дзелÑ\8cнÑ\96кам, Ñ\8fкÑ\96 Ð±Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b $1.\nÐ\9fÑ\80Ñ\8bÑ\87Ñ\8bна Ð³Ñ\8dÑ\82ага:\n\n:<em>$2</em>\n\n* Ð\91лÑ\8fкаванÑ\8cне Ð¿Ð°Ñ\87алоÑ\81Ñ\8f: $8\n* Ð\91лÑ\8fкаванÑ\8cне Ñ\81конÑ\87Ñ\8bÑ\86Ñ\86а: $6\n* Ð\91Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b: $7\n\nÐ\92Ñ\8b Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\81канÑ\82акÑ\82аваÑ\86Ñ\86а Ð· $1 Ñ\86Ñ\96 Ð· Ð°Ð´Ð½Ñ\8bм Ð·Ñ\8c Ñ\96нÑ\88Ñ\8bÑ\85 [[{{MediaWiki:Grouppage-sysop}}|адмÑ\96нÑ\96Ñ\81Ñ\82Ñ\80аÑ\82аÑ\80аÑ\9e]], ÐºÐ°Ð± Ð°Ð±Ð¼ÐµÑ\80каваÑ\86Ñ\8c Ð±Ð»Ñ\8fкаванÑ\8cне.\n\nÐ\97аÑ\9eважÑ\86е, Ñ\88Ñ\82о Ð\92Ñ\8b Ð½Ñ\8f Ð·Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\9eжÑ\8bваÑ\86Ñ\8c Ð¼Ð°Ð³Ñ\87Ñ\8bмаÑ\81Ñ\8cÑ\86Ñ\8c Â«Ð´Ð°Ñ\81лаÑ\86Ñ\8c Ð»Ñ\96Ñ\81Ñ\82 Ð¿Ñ\80аз Ñ\8dлекÑ\82Ñ\80оннÑ\83Ñ\8e Ð¿Ð¾Ñ\88Ñ\82Ñ\83», Ð¿Ð°ÐºÑ\83лÑ\8c Ð½Ñ\8f Ð±Ñ\83дзе Ð¿Ð°Ð·Ð½Ð°Ñ\87анÑ\8b Ð´Ð·ÐµÐ¹Ð½Ñ\8b Ð°Ð´Ñ\80аÑ\81 Ñ\8dлекÑ\82Ñ\80оннай Ð¿Ð¾Ñ\88Ñ\82Ñ\8b Ñ\9e Ð\92аÑ\88Ñ\8bÑ\85 [[Special:Preferences|наладаÑ\85 Ñ\83дзелÑ\8cнÑ\96ка]], Ñ\96 ÐºÐ°Ð»Ñ\96 Ð³Ñ\8dÑ\82а Ð\92ам Ð½Ðµ Ð±Ñ\8bло Ð·Ð°Ð±Ð°Ñ\80онена.\n\nÐ\92аÑ\88 Ñ\86Ñ\8fпеÑ\80аÑ\88нÑ\96 IP-адÑ\80аÑ\81 â\80\94 $3, Ñ\96дÑ\8dнÑ\82Ñ\8bÑ\84Ñ\96каÑ\82аÑ\80 Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f â\80\94 #$5.\nÐ\9aалÑ\96 Ð»Ð°Ñ\81ка, Ñ\83лÑ\83Ñ\87айÑ\86е Ñ\9eÑ\81Ñ\8e Ð²Ñ\8bÑ\88Ñ\8dйпададзенÑ\83Ñ\8e Ñ\96нÑ\84аÑ\80маÑ\86Ñ\8bÑ\8e Ð²Ð° Ñ\9eÑ\81е Ð·Ð°Ð¿Ñ\8bÑ\82Ñ\8b, Ñ\88Ñ\82о Ð\92ы будзеце рабіць.",
+       "blockedtext": "<strong>Ð\92аÑ\88 Ñ\80аÑ\85Ñ\83нак Ñ\83дзелÑ\8cнÑ\96ка Ñ\86Ñ\96 IP-адÑ\80аÑ\81 Ð±Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b.</strong>\n\nÐ\91лÑ\8fкаванÑ\8cне Ð²Ñ\8bканаÑ\9e $1.\nÐ\9fÑ\80Ñ\8bÑ\87Ñ\8bна Ð³Ñ\8dÑ\82ага: <em>$2</em>.\n\n* Ð\9fаÑ\87аÑ\82ак Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f: $8\n* Ð¡ÐºÐ°Ð½Ñ\87Ñ\8dнÑ\8cне Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f: $6\n* Ð\91Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b: $7\n\nÐ\92Ñ\8b Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\81канÑ\82акÑ\82аваÑ\86Ñ\86а Ð· $1 Ñ\86Ñ\96 Ð°Ð´Ð½Ñ\8bм Ð·Ñ\8c Ñ\96нÑ\88Ñ\8bÑ\85 [[{{MediaWiki:Grouppage-sysop}}|адмÑ\96нÑ\96Ñ\81Ñ\82Ñ\80аÑ\82аÑ\80аÑ\9e]], ÐºÐ°Ð± Ð°Ð±Ð¼ÐµÑ\80каваÑ\86Ñ\8c Ð±Ð»Ñ\8fкаванÑ\8cне. Ð\97аÑ\9eважÑ\86е, Ñ\88Ñ\82о Ð²Ñ\8b Ð½Ñ\8f Ð·Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\9eжÑ\8bÑ\86Ñ\8c Ð¼Ð°Ð³Ñ\87Ñ\8bмаÑ\81Ñ\8cÑ\86Ñ\8c Â«{{int:emailuser}}», Ð¿Ð°ÐºÑ\83лÑ\8c Ð½Ðµ Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bÑ\86е Ñ\81апÑ\80аÑ\9eднÑ\8b Ð°Ð´Ñ\80аÑ\81 Ñ\8dлекÑ\82Ñ\80оннай Ð¿Ð¾Ñ\88Ñ\82Ñ\8b Ñ\9e Ð²Ð°Ñ\88Ñ\8bÑ\85 [[Special:Preferences|наладаÑ\85]], Ñ\96 ÐºÐ°Ð»Ñ\96 Ð³Ñ\8dÑ\82а Ð²Ð°Ð¼ Ð½Ðµ Ð±Ñ\8bло Ð·Ð°Ð±Ð°Ñ\80онена.\nÐ\92аÑ\88 IP-адÑ\80аÑ\81 â\80\94 $3, Ñ\96дÑ\8dнÑ\82Ñ\8bÑ\84Ñ\96каÑ\82аÑ\80 Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f â\80\94 #$5.\nÐ\9aалÑ\96 Ð»Ð°Ñ\81ка, Ñ\83лÑ\83Ñ\87айÑ\86е Ñ\9eÑ\81Ñ\8e Ð²Ñ\8bÑ\88Ñ\8dйпададзенÑ\83Ñ\8e Ñ\96нÑ\84аÑ\80маÑ\86Ñ\8bÑ\8e Ð²Ð° Ñ\9eÑ\81е Ð·Ð°Ð¿Ñ\8bÑ\82Ñ\8b, Ñ\88Ñ\82о Ð²ы будзеце рабіць.",
+       "autoblockedtext": "Ð\92аÑ\88 IP-адÑ\80аÑ\81 Ð±Ñ\8bÑ\9e Ð°Ñ\9eÑ\82амаÑ\82Ñ\8bÑ\87на Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b, Ñ\82амÑ\83 Ñ\88Ñ\82о Ñ\91н Ñ\83жÑ\8bваÑ\9eÑ\81Ñ\8f Ñ\96нÑ\88Ñ\8bм Ñ\83дзелÑ\8cнÑ\96кам, Ñ\8fкÑ\96 Ð±Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b $1.\nÐ\9fÑ\80Ñ\8bÑ\87Ñ\8bна Ð³Ñ\8dÑ\82ага:\n\n:<em>$2</em>\n\n* Ð\91лÑ\8fкаванÑ\8cне Ð¿Ð°Ñ\87алоÑ\81Ñ\8f: $8\n* Ð\91лÑ\8fкаванÑ\8cне Ñ\81конÑ\87Ñ\8bÑ\86Ñ\86а: $6\n* Ð\91Ñ\8bÑ\9e Ð·Ð°Ð±Ð»Ñ\8fкаванÑ\8b: $7\n\nÐ\92Ñ\8b Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\81канÑ\82акÑ\82аваÑ\86Ñ\86а Ð· $1 Ñ\86Ñ\96 Ð· Ð°Ð´Ð½Ñ\8bм Ð·Ñ\8c Ñ\96нÑ\88Ñ\8bÑ\85 [[{{MediaWiki:Grouppage-sysop}}|адмÑ\96нÑ\96Ñ\81Ñ\82Ñ\80аÑ\82аÑ\80аÑ\9e]], ÐºÐ°Ð± Ð°Ð±Ð¼ÐµÑ\80каваÑ\86Ñ\8c Ð±Ð»Ñ\8fкаванÑ\8cне.\n\nÐ\97аÑ\9eважÑ\86е, Ñ\88Ñ\82о Ð²Ñ\8b Ð½Ñ\8f Ð·Ð¼Ð¾Ð¶Ð°Ñ\86е Ñ\9eжÑ\8bваÑ\86Ñ\8c Ð¼Ð°Ð³Ñ\87Ñ\8bмаÑ\81Ñ\8cÑ\86Ñ\8c Â«{{int:emailuser}}», Ð¿Ð°ÐºÑ\83лÑ\8c Ð½Ñ\8f Ð±Ñ\83дзе Ð¿Ð°Ð·Ð½Ð°Ñ\87анÑ\8b Ð´Ð·ÐµÐ¹Ð½Ñ\8b Ð°Ð´Ñ\80аÑ\81 Ñ\8dлекÑ\82Ñ\80оннай Ð¿Ð¾Ñ\88Ñ\82Ñ\8b Ñ\9e Ð²Ð°Ñ\88Ñ\8bÑ\85 [[Special:Preferences|наладаÑ\85 Ñ\83дзелÑ\8cнÑ\96ка]], Ñ\96 ÐºÐ°Ð»Ñ\96 Ð³Ñ\8dÑ\82а Ð²Ð°Ð¼ Ð½Ðµ Ð±Ñ\8bло Ð·Ð°Ð±Ð°Ñ\80онена.\n\nÐ\92аÑ\88 Ñ\86Ñ\8fпеÑ\80аÑ\88нÑ\96 IP-адÑ\80аÑ\81 â\80\94 $3, Ñ\96дÑ\8dнÑ\82Ñ\8bÑ\84Ñ\96каÑ\82аÑ\80 Ð±Ð»Ñ\8fкаванÑ\8cнÑ\8f â\80\94 #$5.\nÐ\9aалÑ\96 Ð»Ð°Ñ\81ка, Ñ\83лÑ\83Ñ\87айÑ\86е Ñ\9eÑ\81Ñ\8e Ð²Ñ\8bÑ\88Ñ\8dйпададзенÑ\83Ñ\8e Ñ\96нÑ\84аÑ\80маÑ\86Ñ\8bÑ\8e Ð²Ð° Ñ\9eÑ\81е Ð·Ð°Ð¿Ñ\8bÑ\82Ñ\8b, Ñ\88Ñ\82о Ð²ы будзеце рабіць.",
        "systemblockedtext": "Вашае імя ўдзельніка ці IP-адрас былі аўтаматычна заблякаваныя MediaWiki.\nЗ наступнай прычыны:\n\n:<em>$2</em>\n\n* Пачатак блякаваньня: $8\n* Сканчэньне блякаваньня: $6\n* Мэта блякаваньня: $7\n\nВаш цяперашні IP-адрас — $3.\nКалі ласка, уключайце ўсе пададзеныя вышэй дэталі ва ўсе запыты, што вы робіце.",
        "blockednoreason": "прычына не пазначана",
        "whitelistedittext": "Вам трэба $1, каб рэдагаваць старонкі.",
        "upload": "Загрузіць файл",
        "uploadbtn": "Загрузіць файл",
        "reuploaddesc": "Скасаваць загрузку і вярнуцца да формы загрузкі",
-       "upload-tryagain": "Даслаць зьмененае апісаньне файла",
+       "upload-tryagain": "Даслаць зьмененае апісаньне файлу",
        "upload-tryagain-nostash": "Даслаць паўторна загружаны файл і зьмененае апісаньне",
        "uploadnologin": "Вы не ўвайшлі ў сыстэму",
        "uploadnologintext": "Вам трэба $1, каб загружаць файлы.",
        "upload_directory_missing": "Загрузачная дырэкторыя ($1) адсутнічае і ня можа быць створаная сэрвэрам.",
        "upload_directory_read_only": "Сэрвэр ня мае правоў на запіс у дырэкторыю загружаных файлаў ($1).",
        "uploaderror": "Памылка загрузкі",
-       "upload-recreate-warning": "'''Увага: файл з такой назвай быў выдалены альбо перанесены.'''\n\nЖурнал выдаленьняў і пераносаў гэтай старонкі для зручнасьці пададзены тут:",
+       "upload-recreate-warning": "<strong>Увага: файл з такой назвай быў выдалены альбо перанесены.</strong>\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": "{{PLURAL:$2|Дазволены тып|Дазволеныя тыпы}} файлаў: $1.",
        "upload-preferred": "{{PLURAL:$2|Пажаданы тып|Пажаданыя тыпы}} файлаў: $1.",
index 0333b61..c7d9bf6 100644 (file)
        "recentchangesdays-max": "(найбольш $1 {{PLURAL:$1|дзень|дзён}})",
        "recentchangescount": "Перадвызначаная колькасць правак для паказу:",
        "prefs-help-recentchangescount": "Максімальная колькасць: 1000",
-       "prefs-help-watchlist-token2": "Гэта сакрэтны ключ да сеціўнай стужкі з Вашага спіса назірання.\nКожны, хто ведае гэты ключ, будзе мець магчымасць чытаць Ваш спіс назірання, таму не дзяліцеся ім.\nКалі трэба, можна [[Special:ResetTokens|скінуць яго]].",
+       "prefs-help-watchlist-token2": "Гэта сакрэтны ключ да сеціўнай стужкі з Вашага спісу назірання.\nКожны, хто ведае гэты ключ, будзе мець магчымасць чытаць Ваш спіс назірання, таму не дзяліцеся ім.\nКалі трэба, можна [[Special:ResetTokens|скінуць яго]].",
        "savedprefs": "Настройкі замацаваныя.",
        "savedrights": "Групы {{GENDER:$1|ўдзельніка|ўдзельніцы}} $1 захаваныя.",
        "timezonelegend": "Часавы пояс:",
        "uploaded-event-handler-on-svg": "Устаноўка атрыбутаў апрацоўшчыка падзей <code>$1=\"$2\"</code> у SVG файле не дазваляецца.",
        "uploaded-href-attribute-svg": "у SVG файлах атрыбутам href дазволены толькі мэты віду http:// або https://, знойдзена <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-href-unsafe-target-svg": "У ўкладзеным SVG файле знойдзена спасылка на небяспечныя звесткі: URI мэты <code>&lt;$1 $2=\"$3\"&gt;</code>.",
-       "uploaded-animate-svg": "У ўкладзеным SVG файле знойдзены тэг \"animate\", здольны змяніць спасылку з дапамогай атрыбута \"from\" <code>&lt;$1 $2=\"$3\"&gt;</code>.",
+       "uploaded-animate-svg": "Ð\92а ўкладзеным SVG файле знойдзены тэг \"animate\", здольны змяніць спасылку з дапамогай атрыбута \"from\" <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-setting-event-handler-svg": "Устаноўка атрыбутаў апрацоўкі падзей заблакавана, у ўкладзеным SVG-файле знойдзены код <code>&lt;$1 $2=\"$3\"&gt;</code>.",
        "uploaded-setting-href-svg": "Выкарыстанне тэга \"set\" для дадання атрыбута \"href\" у бацькоўскі элемент заблакавана.",
        "uploaded-wrong-setting-svg": "Ужыванне тэга \"set\" для задання ў якасці мэты аддаленага адраса/звестак/сцэнарыя для любога атрыбута заблакавана. У ўкладзеным SVG-файле знойдзены <code>&lt;set to=\"$1\"&gt;</code>.",
        "tooltip-ca-undelete": "Аднавіць праўкі, зробленыя на гэтай старонцы перад тым, як яна была сцёрта",
        "tooltip-ca-move": "Перанесці гэтую старонку пад іншую назву",
        "tooltip-ca-watch": "Дадаць гэтую старонку да свайго спісу назіраных старонак",
-       "tooltip-ca-unwatch": "Выняць гэту старонку з вашага спіса назірання",
+       "tooltip-ca-unwatch": "Выняць гэтую старонку з Вашага спісу назірання",
        "tooltip-search": "Знайсці ў {{SITENAME}}",
        "tooltip-search-go": "Перайсці да старонкі з дакладна такой назвай, калі такая наогул існуе",
        "tooltip-search-fulltext": "Знайсці гэты тэкст у тэкстах старонак",
        "confirm-watch-button": "ОК",
        "confirm-watch-top": "Дабавіць старонку ў спіс назірання",
        "confirm-unwatch-button": "ОК",
-       "confirm-unwatch-top": "Ð\92Ñ\8bнÑ\8fÑ\86Ñ\8c Ð³Ñ\8dÑ\82Ñ\83 Ñ\81Ñ\82аÑ\80онкÑ\83 Ð· Ð²Ð°Ñ\88ага Ñ\81пÑ\96Ñ\81а назірання?",
+       "confirm-unwatch-top": "Ð\92Ñ\8bнÑ\8fÑ\86Ñ\8c Ð³Ñ\8dÑ\82Ñ\83 Ñ\81Ñ\82аÑ\80онкÑ\83 Ð· Ð\92аÑ\88ага Ñ\81пÑ\96Ñ\81Ñ\83 назірання?",
        "confirm-rollback-button": "Добра",
        "confirm-rollback-top": "Адкаціць праўкі гэтай старонкі?",
        "quotation-marks": "«$1»",
        "autosumm-replace": "Замена старонкі на '$1'",
        "autoredircomment": "Перасылае да [[$1]]",
        "autosumm-removed-redirect": "Выдаленае перенаправление на [[$1]]",
+       "autosumm-changed-redirect-target": "Мэта перасылкі зменена з [[$1]] на [[$2]]",
        "autosumm-new": "Новая старонка: '$1'",
        "autosumm-newblank": "Створана пустая старонка",
        "size-bytes": "$1 {{PLURAL:$1|байт|байты|байтаў}}",
        "watchlistedit-normal-legend": "Выдаленне складнікаў са спісу назірання",
        "watchlistedit-normal-explain": "Назвы старонак з ліку назіраных паказаныя ніжэй. Каб нешта выдаліць, адзначце клетку побач з адпаведным радком, пасля чаго націсніце \"Выняць складнікі\". Таксама можна правіць гэты спіс непасрэдна, [[Special:EditWatchlist/raw|без афармлення]].",
        "watchlistedit-normal-submit": "Выняць складнікі",
-       "watchlistedit-normal-done": "Ð\97 Ð²Ð°Ñ\88ага Ñ\81пÑ\96Ñ\81а назірання {{PLURAL:$1|быў выдалены|былі выдалены|было выдалена}} $1 {{PLURAL:$1|складнік|складнікі|складнікаў}}:",
+       "watchlistedit-normal-done": "Ð\97 Ð\92аÑ\88ага Ñ\81пÑ\96Ñ\81Ñ\83 назірання {{PLURAL:$1|быў выдалены|былі выдалены|было выдалена}} $1 {{PLURAL:$1|складнік|складнікі|складнікаў}}:",
        "watchlistedit-raw-title": "Нефарматаваны спіс назірання",
        "watchlistedit-raw-legend": "Правіць нефарматаваны спіс назірання",
        "watchlistedit-raw-explain": "Назвы старонак з ліку назіраных паказаныя ніжэй, без афармлення, адна назва на адзін радок; такім чынам, спіс можна правіць як звычайны тэкст. Па сканчэнні націсніце \"{{int:Watchlistedit-raw-submit}}\". Таксама гэта можна зрабіць праз [[Special:EditWatchlist|стандартны інтэрфейс]].",
        "logentry-delete-delete": "$1 {{GENDER:$2|выдаліў|выдаліла}} старонку $3",
        "logentry-delete-delete_redir": "$1 {{GENDER:$2|выдаліў|выдаліла}} перанакіраванне $3 шляхам перазапісу",
        "logentry-delete-restore": "$1 {{GENDER:$2|аднавіў|аднавіла}} старонку $3 ($4)",
+       "restore-count-revisions": "{{PLURAL:$1|1 версія|$1 версіі|$1 версій}}",
        "logentry-delete-event": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|запісу журнала|$5 запісаў журнала}} $3: $4",
        "logentry-delete-revision": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць {{PLURAL:$5|версіі|$5 версій|$5 версій}} старонкі $3: $4",
        "logentry-delete-event-legacy": "$1 {{GENDER:$2|змяніў|змяніла}} бачнасць запісаў журнала $3",
index b4b31f2..07b224c 100644 (file)
        "savechanges": "Съхраняване на промените",
        "publishpage": "Публикуване на страницата",
        "publishchanges": "Публикуване на промените",
+       "savearticle-start": "Съхраняване на страницата...",
+       "savechanges-start": "Съхраняване на промените...",
+       "publishpage-start": "Публикуване на страницата...",
        "publishchanges-start": "Публикуване на промените...",
        "preview": "Предварителен преглед",
        "showpreview": "Предварителен преглед",
        "anoneditwarning": "<strong>Внимание:</strong> Не сте влезли в системата. Ако направите редакция IP-адресът Ви ще бъде публично видим. Ако <strong>[$1 влезете]</strong> или си <strong>[$2 създадете акаунт]</strong>, редакциите Ви ще бъдат свързани с потребителското Ви име, заедно с други преимущества.",
        "anonpreviewwarning": "<em>Не сте влезли в системата. Ако съхраните редакцията си, тя ще бъде записана в историята на страницата с вашия IP-адрес.</em>",
        "missingsummary": "<strong>Напомняне:</strong> Не е въведено кратко описание на промените.\nПри повторно натискане на бутона „$1“, редакцията ще бъде съхранена без резюме.",
+       "selfredirect": "<strong>Внимание:</strong> Пренасочвате страница към самата нея.\nМоже би сте посочили грешна цел за пренасочването или може би сте редактирали грешната страница.\nАко натиснете „$1“ отново, пренасочването ще бъде създадено.",
        "missingcommenttext": "Моля, въведете коментар.",
        "missingcommentheader": "<strong>Напомняне:</strong> Не е въведено заглавие на коментара.\nПри повторно натискане на „$1“, редакцията ще бъде записана без коментар.",
        "summary-preview": "Предварителен преглед на резюмето:",
        "blockedtitle": "Потребителят е блокиран",
        "blockedtext": "'''Вашето потребителско име (или IP-адрес) беше блокирано.'''\n\nБлокирането е извършено от $1. Посочената причина е: ''$2''\n\n*Начало на блокирането: $8\n*Край на блокирането: $6\n*Блокирането се отнася за: $7\n\nМожете да се свържете с $1 или с някой от останалите [[{{MediaWiki:Grouppage-sysop}}|администратори]], за да обсъдите блокирането.\n\nМожете да използвате услугата „Пращане писмо на потребител“ само ако не ви е забранена употребата ѝ и ако сте посочили валидна електронна поща в [[Special:Preferences|настройките]] си.\n\nВашият IP адрес е $3, а номерът на блокирането е $5. Включвайте едно от двете или и двете във всяко запитване, което правите.",
        "autoblockedtext": "IP-адресът ви беше блокиран автоматично, защото е бил използван от друг потребител, който е бил блокиран от $1.\nПосочената причина е:\n\n:''$2''\n\n* Начало на блокирането: $8\n* Край на блокирането: $6\n* Блокирането се отнася за: $7\n\nМожете да се свържете с $1 или с някой от останалите [[{{MediaWiki:Grouppage-sysop}}|администратори]], за да обсъдите блокирането.\n\nМожете да използвате услугата „Пращане писмо на потребител“ само ако не ви е забранена употребата ѝ и ако сте посочили валидна електронна поща в [[Special:Preferences|настройките]] си.\n\nТекущият ви IP-адрес е $3, а номерът на блокирането ви е $5. Включвайте ги във всяко питане, което правите.",
+       "systemblockedtext": "Вашето потребителско име или IP адрес беше автоматично блокирано от Медия Уики.\nПосочената причина е:\n\n:<em>$2</em>\n\n* Начало на блокирането: $8\n* Край на блокирането: $6\n* Блокирането се отнася за: $7\n\nВашият текущ IP адрес е $3.\nМоля, включете всичките детайли по-горе, ако правите каквито и да е запитвания.",
        "blockednoreason": "не е указана причина",
        "whitelistedittext": "Редактирането на страници изисква $1 в системата.",
        "confirmedittext": "Необходимо е да потвърдите електронната си поща, преди да редактирате страници.\nВъведете и потвърдете адреса си на [[Special:Preferences|страницата с настройките]].",
        "prefs-dateformat": "Формат на датата",
        "prefs-timeoffset": "Часово отместване",
        "prefs-advancedediting": "Общи настройки",
+       "prefs-developertools": "Инструменти за разработчици",
        "prefs-editor": "Редактор",
        "prefs-preview": "Преглед",
        "prefs-advancedrc": "Разширени настройки",
        "rcfilters-filter-humans-description": "Редакции, направени от редактори.",
        "rcfilters-filtergroup-reviewstatus": "Проверка на статуса",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "Непатрулирано",
+       "rcfilters-filter-reviewstatus-manual-label": "Ръчно патрулирани",
+       "rcfilters-filter-reviewstatus-auto-label": "Автоматично патрулирани",
        "rcfilters-filtergroup-significance": "Значимост",
        "rcfilters-filter-minor-label": "Малки промени",
        "rcfilters-filter-minor-description": "Редакции, които не са отбелязани като малки промени.",
        "filedelete-intro-old": "Изтривате версията на <strong>[[Media:$1|$1]]</strong> към [$4 $3, $2].",
        "filedelete-comment": "Причина:",
        "filedelete-submit": "Изтриване",
-       "filedelete-success": "Файлът '''$1''' беше изтрит.",
+       "filedelete-success": "Файлът <strong>$1</strong> беше изтрит.",
        "filedelete-success-old": "Версията на <strong>[[Media:$1|$1]]</strong> към $3, $2 е била изтрита.",
        "filedelete-nofile": "Файлът <strong>$1</strong> не съществува.",
-       "filedelete-nofile-old": "Не съществува архивна версия на '''$1''' с указаните параметри.",
+       "filedelete-nofile-old": "Не съществува архивна версия на <strong>$1</strong> с указаните параметри.",
        "filedelete-otherreason": "Друга/допълнителна причина:",
        "filedelete-reason-otherlist": "Друга причина",
        "filedelete-reason-dropdown": "*Общи причини за изтриване\n** Нарушение на авторските права\n** Файлът се повтаря",
        "protect-othertime": "Друг срок:",
        "protect-othertime-op": "друг срок",
        "protect-existing-expiry": "Оставащо време: $2, $3",
-       "protect-existing-expiry-infinity": "Existing expiration time: безсрочно",
+       "protect-existing-expiry-infinity": "Оставащо време: безсрочно",
        "protect-otherreason": "Друга/допълнителна причина:",
        "protect-otherreason-op": "Друга причина",
        "protect-dropdown": "* Стандартни причини за защита на страници\n** Чест обект на вандализъм\n** Чест обект на спам\n** Редакторска война\n** Страница, изискваща много сървърни ресурси",
        "restriction-level-all": "всички",
        "undelete": "Преглед на изтрити страници",
        "undeletepage": "Преглед и възстановяване на изтрити страници",
-       "undeletepagetitle": "'''По-долу е показан списък на изтритите версии на [[:$1|$1]]'''.",
+       "undeletepagetitle": "<strong>По-долу е показан списък на изтритите версии на [[:$1|$1]]/strong>.",
        "viewdeletedpage": "Преглед на изтрити страници",
        "undeletepagetext": "{{PLURAL:$1|Следната страница беше изтрита, но все още се намира в архива и може да бъде възстановена|Следните $1 страници бяха изтрити, но все още се намират в архива и могат да бъдат възстановени}}. Архивът може да се почиства от време на време.",
        "undelete-fieldset-title": "Възстановяване на версии",
index f4158ea..a8a60d5 100644 (file)
        "botpasswords-restriction-failed": "Les restriccions de contrasenyes de bots impedeixen aquest inici de sessió.",
        "botpasswords-invalid-name": "El nom d'usuari especificat no conté el separador de contrasenya de bot («$1»).",
        "botpasswords-not-exist": "L'usuari «$1» no té una contrasenya de bot anomenada «$2».",
+       "botpasswords-needs-reset": "Cal reinicialitzar la contrasenya del robot «$2» que pertany a {{GENDER:$1|l’usuari|la usuària}} «$1».",
        "resetpass_forbidden": "No poden canviar-se les contrasenyes",
        "resetpass_forbidden-reason": "Les contrasenyes no es poden canviar: $1",
        "resetpass-no-info": "Heu d'estar registrats en un compte per a poder accedir directament a aquesta pàgina.",
index 79f2b2b..b23fbab 100644 (file)
        "rcfilters-limit-and-date-label": "$1 گۆڕانکاری، $2",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|ڕۆژ}}",
        "rcfilters-quickfilters": "پاڵوێنە پاشەکەوتکراوەکان",
+       "rcfilters-quickfilters-placeholder-title": "ھیچ پاڵوێنەیەک پاشەکەوت نەکراوە",
+       "rcfilters-quickfilters-placeholder-description": "بۆ پاشەکەوتکردنی ھەڵبژاردەی پاڵوێنەکان و دووبارە بەکارھێنانەوەیان، کرتە لەسەر نیشانی نیشانەی کتێبەکە بکە.",
        "rcfilters-savedqueries-defaultlabel": "پاڵوێنە پاشەکەوتکراوەکان",
        "rcfilters-savedqueries-setdefault": "بە بنەڕەتی کارای بکە",
        "rcfilters-savedqueries-new-name-label": "ناو",
index a37e90a..0ca1120 100644 (file)
        "subject-preview": "Náhled předmětu:",
        "previewerrortext": "Při pokusu o zobrazení náhledu vašich změn došlo k chybě.",
        "blockedtitle": "Uživatel zablokován",
-       "blockedtext": "<strong>Vaší IP adrese či uživatelskému jménu byla zablokována možnost editace.</strong>\n\nZablokování {{GENDER:$4|provedl|provedla}} $1.\nUdaným důvodem bylo <em>$2</em>.\n\n* Začátek blokování: $8\n* Zablokování vyprší: $6\n* Blokovaný uživatel: $7\n\nPokud chcete zablokování prodiskutovat, můžete kontaktovat {{GENDER:$4|uživatele|uživatelku}} $1 či jiného [[{{MediaWiki:Grouppage-sysop}}|správce]].\nUvědomte si, že nemůžete použít funkci „Poslat e-mail“, jestliže nemáte ve svém [[Special:Preferences|nastavení]] uvedenu platnou e-mailovou adresu nebo pokud vám byla tato možnost zakázána.\nVaše IP adresa je $3 a&nbsp;identifikační číslo bloku je #$5; tyto údaje uvádějte ve všech dotazech na správce.",
-       "autoblockedtext": "Vaše IP adresa byla automaticky zablokována, protože ji používal jiný uživatel, kterého zablokoval $1.\nUdaný důvod blokování:\n\n:<em>$2</em>\n\n* Začátek blokování: $8\n* Konec blokování: $6\n* Původně blokovaný uživatel: $7\n\nZablokování můžete prodiskutovat se správcem $1 nebo některým z dalších [[{{MediaWiki:Grouppage-sysop}}|správců]].\n\nUvědomte si však, že funkci „Poslat e-mail tomuto uživateli“ nemůžete použít, pokud nemáte ve svém [[Special:Preferences|uživatelském nastavení]] zadaný platný e-mail a nebylo vám zablokováno jeho užívání.\n\nVaše současná IP adresa je $3, číslo vašeho zablokování je #$5.\nProsíme, uveďte tyto údaje při komunikaci se správci.",
+       "blockedtext": "<strong>Vaší IP adrese či uživatelskému jménu byla zablokována možnost editace.</strong>\n\nZablokování {{GENDER:$4|provedl|provedla}} $1.\nUdaným důvodem bylo <em>$2</em>.\n\n* Začátek blokování: $8\n* Zablokování vyprší: $6\n* Blokovaný uživatel: $7\n\nPokud chcete zablokování prodiskutovat, můžete kontaktovat {{GENDER:$4|uživatele|uživatelku}} $1 či jiného [[{{MediaWiki:Grouppage-sysop}}|správce]].\nUvědomte si, že nemůžete použít funkci „{{int:emailuser}}“, jestliže nemáte ve svém [[Special:Preferences|nastavení]] uvedenu platnou e-mailovou adresu nebo pokud vám byla tato možnost zakázána.\nVaše IP adresa je $3 a&nbsp;identifikační číslo bloku je #$5; tyto údaje uvádějte ve všech dotazech na správce.",
+       "autoblockedtext": "Vaše IP adresa byla automaticky zablokována, protože ji používal jiný uživatel, kterého zablokoval $1.\nUdaný důvod blokování:\n\n:<em>$2</em>\n\n* Začátek blokování: $8\n* Konec blokování: $6\n* Původně blokovaný uživatel: $7\n\nZablokování můžete prodiskutovat se správcem $1 nebo některým z dalších [[{{MediaWiki:Grouppage-sysop}}|správců]].\n\nUvědomte si však, že funkci „{{int:emailuser}}“ nemůžete použít, pokud nemáte ve svém [[Special:Preferences|uživatelském nastavení]] zadaný platný e-mail a nebylo vám zablokováno jeho užívání.\n\nVaše současná IP adresa je $3, číslo vašeho zablokování je #$5.\nProsíme, uveďte tyto údaje při komunikaci se správci.",
        "systemblockedtext": "Vaše IP adresa byla automaticky zablokována softwarem MediaWiki.\nUdaný důvod blokování:\n\n:<em>$2</em>\n\n* Začátek blokování: $8\n* Konec blokování: $6\n* Původně blokovaný uživatel: $7\n\nVaše současná IP adresa je $3.\nProsíme, uveďte tyto údaje při komunikaci se správci.",
        "blockednoreason": "důvod nebyl zadán",
        "whitelistedittext": "Pro editaci se musíte $1.",
index 946c6c9..4de9d8c 100644 (file)
        "tooltip-pt-logout": "Сеансне пĕтер",
        "tooltip-pt-createaccount": "Аккаунт ту та системӑна кӗр. Паллах, унсӑрах та юрать, анчах та аккаунтпа кӗни лайӑхрах.",
        "tooltip-ca-talk": "Статьяна сӳтсе явасси",
-       "tooltip-ca-edit": "Эле тӳрлет",
+       "tooltip-ca-edit": "Ð\9aÄ\83на тӳрлет",
        "tooltip-ca-addsection": "Çĕнĕ пай ту",
        "tooltip-ca-viewsource": "Ку страницӑна эсир улӑштарма пултараймастӑр. Ӑна мӗнле ҫырнине кӑна пӑхма пултаратӑр.",
-       "tooltip-ca-history": "Эле Ñ\83лÓ\91Ñ\88Ñ\82аÑ\80нин ÐºÑ\83н-Ò«Ñ\83лÓ\97",
+       "tooltip-ca-history": "Ð\9aÑ\83нÄ\83н Ñ\83лÓ\91Ñ\88Ä\83нниÑ\81ем",
        "tooltip-ca-protect": "Улӑшратусенчен сыхласси",
        "tooltip-ca-delete": "Страницӑна кӑларса пӑрахмалли",
        "tooltip-ca-move": "Страницӑна урӑх ҫӗре куҫарасси",
index 0470691..a820ad5 100644 (file)
        "subject-preview": "Vorschau der Zusammenfassungszeile:",
        "previewerrortext": "Beim Versuch, eine Vorschau deiner Änderungen anzuzeigen, ist ein Fehler aufgetreten.",
        "blockedtitle": "Benutzer ist gesperrt",
-       "blockedtext": "'''Dein Benutzername oder deine IP-Adresse wurde gesperrt.'''\n\nDie Sperrung wurde vom Administrator $1 durchgeführt.\nAls Grund wurde ''$2'' angegeben.\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\nDu kannst die „E-Mail an diesen Benutzer“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\nDeine aktuelle IP-Adresse ist $3 und die Sperrkennung lautet $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
-       "autoblockedtext": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.\nAls Grund wurde angegeben:\n\n:''$2''\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\n\nDu kannst die „E-Mail an diesen Benutzer“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\n\nDeine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
+       "blockedtext": "'''Dein Benutzername oder deine IP-Adresse wurde gesperrt.'''\n\nDie Sperrung wurde vom Administrator $1 durchgeführt.\nAls Grund wurde ''$2'' angegeben.\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\nDu kannst die „{{int:emailuser}}“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\nDeine aktuelle IP-Adresse ist $3 und die Sperrkennung lautet $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
+       "autoblockedtext": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.\nAls Grund wurde angegeben:\n\n:''$2''\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\n\nDu kannst die „{{int:emailuser}}“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\n\nDeine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
        "systemblockedtext": "Dein Benutzername oder deine IP-Adresse wurde von MediaWiki automatisch gesperrt.\nDer angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der Sperre: $6\n* Sperre betrifft: $7\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
        "blockednoreason": "keine Begründung angegeben",
        "whitelistedittext": "Du musst dich $1, um Seiten bearbeiten zu können.",
index 2ad2c5a..1a80395 100644 (file)
        "rcnotefrom": "Cêr de <strong>$2</strong> ra nata {{PLURAL:$5|vurnayışiyê}} asenê (tewr vêşi <strong>$1</strong> asenê) <strong>$3, $4</strong>",
        "rclistfrom": "$3 sehat $2 ra tepiya vurnayışanê neweyan bımotne",
        "rcshowhideminor": "Vırnayışê werdiy $1",
-       "rcshowhideminor-show": "Bımotne",
+       "rcshowhideminor-show": "Bımocne",
        "rcshowhideminor-hide": "Bınımne",
        "rcshowhidebots": "botan $1",
-       "rcshowhidebots-show": "Bımotne",
+       "rcshowhidebots-show": "Bımocne",
        "rcshowhidebots-hide": "Bınımne",
        "rcshowhideliu": "karberê qeydbiyay $1",
        "rcshowhideliu-show": "Bımocne",
        "rcshowhideliu-hide": "Bınımne",
        "rcshowhideanons": "$1 karberê bênamey",
-       "rcshowhideanons-show": "Bımotne",
+       "rcshowhideanons-show": "Bımocne",
        "rcshowhideanons-hide": "Bınımne",
        "rcshowhidepatr": "$1 vurnayışê ke dewriya geyrayê",
        "rcshowhidepatr-show": "Bımocne",
        "rcshowhidepatr-hide": "Bınımne",
        "rcshowhidemine": "vırnayışê mı $1",
-       "rcshowhidemine-show": "Bımotne",
+       "rcshowhidemine-show": "Bımocne",
        "rcshowhidemine-hide": "Bınımne",
        "rcshowhidecategorization": "kategorizasyoni $1",
-       "rcshowhidecategorization-show": "Bımotné",
+       "rcshowhidecategorization-show": "Bımocne",
        "rcshowhidecategorization-hide": "Bınımne",
        "rclinks": "$1 vurnayışê peyênê ke $2 rocanê peyênan de biyê, inan bımocne",
        "diff": "ferq",
        "hist": "verên",
        "hide": "Bınımne",
-       "show": "Bımotne",
+       "show": "Bımocne",
        "minoreditletter": "q",
        "newpageletter": "N",
        "boteditletter": "b",
        "withoutinterwiki": "Perrê ke zıwananê binan rê gıreyê cı çıni yo",
        "withoutinterwiki-summary": "Enê pelî ke versiyonê ziwanî binî ra link nidano.",
        "withoutinterwiki-legend": "Verole",
-       "withoutinterwiki-submit": "Bımotne",
+       "withoutinterwiki-submit": "Bımocne",
        "fewestrevisions": "Perrê kı tewr tayn timaryayê",
        "nbytes": "$1 {{PLURAL:$1|bayt|bayti}}",
        "ncategories": "$1 {{PLURAL:$1|Kategori|Kategoriy}}",
        "usereditcount": "$1 {{PLURAL:$1|vurnayîş|vurnayîşî}}",
        "usercreated": "$2 de $1 {{GENDER:$3|viraziya}}",
        "newpages": "Perrê newey",
-       "newpages-submit": "Bımotne",
+       "newpages-submit": "Bımocne",
        "newpages-username": "Nameyê karberi:",
        "ancientpages": "Tewr pelê kıhani",
        "move": "Bıkırışe",
        "specialloguserlabel": "Kerdoğ:",
        "speciallogtitlelabel": "Meqsed (sername ya zi {{ns:user}}:karberi rê nameyê karberi):",
        "log": "Qeydi",
-       "logeventslist-submit": "Bımotne",
+       "logeventslist-submit": "Bımocne",
        "all-logs-page": "Heme qeydê pêroyi",
        "alllogstext": "qey {{SITENAME}}i mocnayişê heme rocaneyani.\ntipa rocaneyi, nameyê karberi (herfa pil u qıci re hessas a), ya zi peli (reyna hessasiyê herfa pil u qıciyi) bıweçine u esayiş qıc kerê.",
        "logempty": "Qeydan dı malumato unasin çıni yo.",
        "linksearch-line": "$1, $2 ra link biya",
        "linksearch-error": "jokeri têna nameyê makina ya serekini de aseni/eseni.",
        "listusersfrom": "karber ê ke pey ıney detpêkeni ramocın:",
-       "listusers-submit": "Bımotne",
+       "listusers-submit": "Bımocne",
        "listusers-noresult": "karber nêdiyayo/a.",
        "listusers-blocked": "(blok biy)",
        "activeusers": "Lista karberanê aktifan",
        "wlnote": "$3 saete $4 ra dıme {{PLURAL:$2|yew saete de|'''$2''' saetan de}} {{PLURAL:$1|vurnayışo peyên|vurnayışê '''$1''' peyêni}} cêrderê.",
        "wlshowlast": "Peyni de  $1 seata u $2 roca  bıasne",
        "watchlist-hide": "Bınımne",
-       "watchlist-submit": "Bımotne",
+       "watchlist-submit": "Bımocne",
        "wlshowtime": "Periyoda zemani asenayışi:",
        "wlshowhideminor": "vırnayışê werdiy",
        "wlshowhidebots": "boti",
        "delete-confirm": "\"$1\" bestere",
        "delete-legend": "Bestere",
        "historywarning": "'''Teme:''' Pela ke şıma esterenê tede yew viyarte be teqriben $1 {{PLURAL:$1|versiyon esto|versiyoni estê}}:",
-       "historyaction-submit": "Bımotne",
+       "historyaction-submit": "Bımocne",
        "confirmdeletetext": "Tı ho yew pele u tarixê pele wederneno.\nTı ra rica keno, tı zani tı ho sekeno, tı zani neticeyanê eno wedarnayışi u tı zani tı ser [[{{MediaWiki:Policy-url}}|poliçe]] kar keno.",
        "actioncomplete": "Kar bi temam",
        "actionfailed": "kar nêbı",
index 4befdc5..43f9386 100644 (file)
        "subject-preview": "Preview of subject:",
        "previewerrortext": "An error occurred while attempting to preview your changes.",
        "blockedtitle": "User is blocked",
-       "blockedtext": "<strong>Your username or IP address has been blocked.</strong>\n\nThe block was made by $1.\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou can contact $1 or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the block.\nYou cannot use the \"email this user\" feature unless a valid email address is specified in your [[Special:Preferences|account preferences]] and you have not been blocked from using it.\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
-       "autoblockedtext": "Your IP address has been automatically blocked because it was used by another user, who was blocked by $1.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou may contact $1 or one of the other [[{{MediaWiki:Grouppage-sysop}}|administrators]] to discuss the block.\n\nNote that you may not use the \"email this user\" feature unless you have a valid email address registered in your [[Special:Preferences|user preferences]] and you have not been blocked from using it.\n\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
+       "blockedtext": "<strong>Your username or IP address has been blocked.</strong>\n\nThe block was made by $1.\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou can contact $1 or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the block.\nYou cannot use the \"{{int:emailuser}}\" feature unless a valid email address is specified in your [[Special:Preferences|account preferences]] and you have not been blocked from using it.\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
+       "autoblockedtext": "Your IP address has been automatically blocked because it was used by another user, who was blocked by $1.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou may contact $1 or one of the other [[{{MediaWiki:Grouppage-sysop}}|administrators]] to discuss the block.\n\nNote that you may not use the \"{{int:emailuser}}\" feature unless you have a valid email address registered in your [[Special:Preferences|user preferences]] and you have not been blocked from using it.\n\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
        "systemblockedtext": "Your username or IP address has been automatically blocked by MediaWiki.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYour current IP address is $3.\nPlease include all above details in any queries you make.",
        "blockednoreason": "no reason given",
        "whitelistedittext": "Please $1 to edit pages.",
index 7353f90..58cf7dd 100644 (file)
        "botpasswords-existing": "Ekzistantaj robotaj pasvortoj",
        "botpasswords-createnew": "Krei novan robotan pasvorton",
        "botpasswords-editexisting": "Redakti ekzistantan robotan pasvorton",
+       "botpasswords-label-needsreset": "(oni devas rekomencigi la pasvorton)",
        "botpasswords-label-appid": "Robota nomo:",
        "botpasswords-label-create": "Krei",
        "botpasswords-label-update": "Ĝisdatigi",
index 7199df9..8360c61 100644 (file)
        "botpasswords-existing": "Contraseñas de bots existentes",
        "botpasswords-createnew": "Crear una contraseña de robot nueva",
        "botpasswords-editexisting": "Editar una contraseña de robot existente",
+       "botpasswords-label-needsreset": "(la contraseña debe restablecerse)",
        "botpasswords-label-appid": "Nombre del robot:",
        "botpasswords-label-create": "Crear",
        "botpasswords-label-update": "Actualizar",
        "botpasswords-restriction-failed": "Las restricciones de la contraseña de bot impiden este inicio de sesión.",
        "botpasswords-invalid-name": "El nombre de usuario especificado no contiene el separador de contraseña de bot (\"$1\").",
        "botpasswords-not-exist": "El usuario \"$1\" no tiene una contraseña de bot llamada \"$2\".",
+       "botpasswords-needs-reset": "Se debe restablecer la contraseña del robot «$2», propiedad {{GENDER:$1|del usuario|de la usuaria}} «$1».",
        "resetpass_forbidden": "No se pueden cambiar las contraseñas",
        "resetpass_forbidden-reason": "Las contraseñas no pueden cambiarse: $1",
        "resetpass-no-info": "Debes iniciar sesión para acceder directamente a esta página.",
        "previewerrortext": "Se ha producido un error al intentar la vista previa de los cambios.",
        "blockedtitle": "El usuario está bloqueado",
        "blockedtext": "<strong>Tu nombre de usuario o dirección IP ha sido bloqueada.</strong>\n\nEl bloqueo lo hizo $1.\nLa razón dada es <em>$2</em>.\n\n* Inicio del bloqueo: $8\n* Caducidad del bloqueo: $6\n* Bloqueo destinado a: $7\n\nPuedes contactar a $1 o con otro de los [[{{MediaWiki:Grouppage-sysop}}|administradores]] para discutir el bloqueo.\nNo puedes utilizar la función «enviar correo electrónico a este usuario» a menos que tengas una dirección de correo electrónico válida registrada en tus [[Special:Preferences|preferencias de usuario]] y la función no haya sido también bloqueada.\n\nTu dirección IP actual es $3, y el identificador del bloqueo es #$5.\nIncluye todos los datos aquí mostrados en cualquier consulta que hagas.",
-       "autoblockedtext": "Tu dirección IP ha sido bloqueada automáticamente porque fue utilizada por otro usuario, que resultó bloqueado por $1.\nEl motivo dado es el siguiente:\n\n:<em>$2</em>\n\n* Inicio del bloqueo: $8\n* Caducidad del bloqueo: $6\n* Bloqueo destinado a: $7\n\nPuedes contactar con $1 o con otro de los [[{{MediaWiki:Grouppage-sysop}}|administradores]] para discutir el bloqueo.\n\nTen en cuenta que no puedes utilizar la función «enviar correo electrónico a este usuario» a menos que tengas una dirección de correo electrónico válida registrada en tus [[Special:Preferences|preferencias de usuario]] y la función no haya sido también bloqueada.\n\nTu dirección IP actual es $3, y el identificador del bloqueo es #$5.\nIncluye todos los datos aquí mostrados en cualquier consulta que hagas.",
+       "autoblockedtext": "Tu dirección IP ha sido bloqueada automáticamente porque fue utilizada por otro usuario, que resultó bloqueado por $1.\nEl motivo dado es el siguiente:\n\n:<em>$2</em>\n\n* Inicio del bloqueo: $8\n* Caducidad del bloqueo: $6\n* Bloqueo destinado a: $7\n\nPuedes contactar con $1 o con otro de los [[{{MediaWiki:Grouppage-sysop}}|administradores]] para discutir el bloqueo.\n\nObserva que no puedes utilizar la función «{{int:emailuser}}» a menos que hayas registrado una dirección de correo electrónico válida en tus [[Special:Preferences|preferencias de usuario]] y la función no haya sido también bloqueada.\n\nTu dirección IP actual es $3, y el identificador del bloqueo es n.º $5.\nIncluye todos los datos aquí mostrados en cualquier consulta que hagas.",
        "systemblockedtext": "Tu nombre de usuario o dirección IP ha sido bloqueado automáticamente por el software MediaWiki.\nLa razón dada es:\n\n:<em>$2</em>\n\n* Inicio del bloqueo: $8\n* Caducidad de bloqueo: $6\n* Destinatario del bloqueo: $7\n\nTu dirección IP actual es $3.\nPor favor, incluye todos los datos aquí mostrados en cualquier consulta que hagas.",
        "blockednoreason": "no se ha especificado el motivo",
        "whitelistedittext": "Tienes que $1 para editar páginas.",
index 0e22f6f..3c5ef67 100644 (file)
        "subject-preview": "Aiheotsikon esikatselu:",
        "previewerrortext": "Muokkaustesi esikatselun toteuttamisessa on tapahtunut virhe.",
        "blockedtitle": "Käyttäjä on estetty",
-       "blockedtext": "'''Käyttäjätunnuksesi tai IP-osoitteesi on estetty.'''\n\nEston on asettanut $1.\nSyy: '''$2'''\n\n* Eston alkamisaika: $8\n* Eston päättymisaika: $6\n* Kohde: $7\n\nVoit keskustella ylläpitäjän $1 tai toisen [[{{MediaWiki:Grouppage-sysop}}|ylläpitäjän]] kanssa estosta.\nHuomaa, ettet voi lähettää sähköpostia {{GRAMMAR:genitive|{{SITENAME}}}} kautta, ellet ole asettanut olemassa olevaa sähköpostiosoitetta [[Special:Preferences|asetuksissa]] tai jos esto on asetettu koskemaan myös sähköpostin lähettämistä.\nIP-osoitteesi on $3 ja estotunnus on #$5.\nLiitä kaikki yllä olevat tiedot mahdollisiin kyselyihisi.",
-       "autoblockedtext": "IP-osoitteesi on estetty automaattisesti, koska sitä on käyttänyt toinen käyttäjä, jonka on estänyt ylläpitäjä $1.\nEston syy on:\n\n:''$2''\n\n* Eston alkamisaika: $8\n* Eston päättymisaika: $6\n* Kohde: $7\n\nVoit keskustella ylläpitäjän $1 tai toisen [[{{MediaWiki:Grouppage-sysop}}|ylläpitäjän]] kanssa estosta.\n\nHuomaa, ettet voi lähettää sähköpostia {{GRAMMAR:genitive|{{SITENAME}}}} kautta, ellet ole asettanut olemassa olevaa sähköpostiosoitetta [[Special:Preferences|asetuksissa]] tai jos esto on asetettu koskemaan myös sähköpostin lähettämistä.\n\nIP-osoitteesi on $3 ja estotunnus on #$5.\nLiitä kaikki yllä olevat tiedot mahdollisiin kyselyihisi.",
+       "blockedtext": "<strong>Käyttäjätunnuksesi tai IP-osoitteesi on estetty.</strong>\n\nEston on asettanut $1.\nAnnettu syy on <em>$2</em>.\n\n* Eston alkamisaika: $8\n* Eston päättymisaika: $6\n* Kohde: $7\n\nVoit keskustella ylläpitäjän $1 tai toisen [[{{MediaWiki:Grouppage-sysop}}|ylläpitäjän]] kanssa estosta.\nHuomaa, ettet voi lähettää sähköpostia {{GRAMMAR:genitive|{{SITENAME}}}} kautta, ellet ole asettanut olemassa olevaa sähköpostiosoitetta [[Special:Preferences|asetuksissa]] tai jos esto on asetettu koskemaan myös sähköpostin lähettämistä.\nIP-osoitteesi on $3 ja estotunnus on #$5.\nLiitä kaikki yllä olevat tiedot mahdollisiin kyselyihisi.",
+       "autoblockedtext": "IP-osoitteesi on estetty automaattisesti, koska sitä on käyttänyt toinen käyttäjä, jonka on estänyt ylläpitäjä $1.\nAnnettu syy on:\n\n:<em>$2</em>\n\n* Eston alkamisaika: $8\n* Eston päättymisaika: $6\n* Kohde: $7\n\nVoit keskustella ylläpitäjän $1 tai toisen [[{{MediaWiki:Grouppage-sysop}}|ylläpitäjän]] kanssa estosta.\n\nHuomaa, ettet voi lähettää sähköpostia {{GRAMMAR:genitive|{{SITENAME}}}} kautta, ellet ole asettanut olemassa olevaa sähköpostiosoitetta [[Special:Preferences|asetuksissa]] tai jos esto on asetettu koskemaan myös sähköpostin lähettämistä.\n\nIP-osoitteesi on $3 ja estotunnus on #$5.\nLiitä kaikki yllä olevat tiedot mahdollisiin kyselyihisi.",
        "systemblockedtext": "Käyttäjätunnuksesi tai IP-osoitteesi on automaattisesti estetty MediaWikin toimesta.\nAnnettu syy on:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nTämänhetkinen IP-osoitteesi on $3.\nOle hyvä ja liitä kaikki yllä olevat tiedot mahdollisiin kyselyihisi.",
        "blockednoreason": "(syytä ei annettu)",
        "whitelistedittext": "Sinun täytyy $1, jotta voisit muokata sivuja.",
index 783a778..5748adf 100644 (file)
        "botpasswords-restriction-failed": "Les restrictions de mot de passe de robots empêchent cette connexion.",
        "botpasswords-invalid-name": "Le nom d’utilisateur spécifié ne contient pas de séparateur de mot de passe de robots (« $1 »).",
        "botpasswords-not-exist": "L’{{GENDER:$1|utilisateur|utilisatrice}} « $1 » n’a pas de mot de passe de robot nommé « $2 ».",
-       "botpasswords-needs-reset": "Le mot de passe du robot de nom \"$2\" de l'utilisat{{GENDER:$1|eur}} \"$1\" doit être réinitialisé.",
+       "botpasswords-needs-reset": "Le mot de passe du robot de nom « $2 » de l’utilisat{{GENDER:$1|eur|rice}} « $1 » doit être réinitialisé.",
        "resetpass_forbidden": "Les mots de passe ne peuvent pas être changés",
        "resetpass_forbidden-reason": "Les mots de passe ne peuvent pas être modifiés : $1",
        "resetpass-no-info": "Vous devez être connecté(e) pour accéder directement à cette page.",
index 584759b..abe2cb3 100644 (file)
        "unprotect": "Chanjé protèksyon-an",
        "newpage": "Nouvèl paj",
        "talkpagelinktext": "diskisyon",
-       "specialpage": "Paj spésyal",
+       "specialpage": "Paj èspésyal",
        "personaltools": "Zouti pèrsonèl",
        "talk": "Diskisyon",
        "views": "Afichaj",
        "nstab-main": "Paj",
        "nstab-user": "Paj di {{GENDER:{{ROOTPAGENAME}}|itilizatò|itilizatris}}",
        "nstab-media": "Médja",
-       "nstab-special": "Paj spésyal",
+       "nstab-special": "Paj èspésyal",
        "nstab-project": "À propo",
        "nstab-image": "Fiché",
        "nstab-mediawiki": "Mésaj",
        "mainpage-nstab": "Paj prensipal",
        "nosuchaction": "Aksyon enkonèt",
        "nosuchactiontext": "Aksyon-an spésifyé andan URL-a sa envalid.\nZòt pitèt mal antré URL-a ou swivi roun lyen éroné.\nLi pé égalman endiké oun anomali andan logisyèl itilizé pa {{SITENAME}}.",
-       "nosuchspecialpage": "Paj spésyal inègzistant",
-       "nospecialpagetext": "<strong>Zòt doumandé oun paj spésyal ki pa ka ègzisté.</strong>\n\nOun lis dé paj spésyal valid ka trouvé so kò asou [[Special:SpecialPages|{{int:specialpages}}]].",
+       "nosuchspecialpage": "Paj èspésyal inègzistant",
+       "nospecialpagetext": "<strong>Zòt doumandé oun paj èspésyal ki pa ka ègzisté.</strong>\n\nOun lis dé paj èspésyal valid ka trouvé so kò asou [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Érò",
        "databaseerror": "Érò di baz di doné",
        "databaseerror-text": "Oun érò di rékèt di baz di doné aparèt.\nSala pé provini di roun anomali annan lojisyèl-a.",
        "mycustomjsprotected": "Zòt pa gen drwè di modifyé sa paj JavaScript.",
        "myprivateinfoprotected": "Zòt pa gen drwè di modifyé zòt enfòrmasyon pèrsonèl.",
        "mypreferencesprotected": "Zòt pa gen drwè di modifyé zòt préférans.",
-       "ns-specialprotected": "Paj spésyal-ya pa pouvé sa modifyé.",
+       "ns-specialprotected": "Paj èspésyal-ya pa pouvé fika modifyé.",
        "titleprotected": "Sa tit té protéjé kont tout kréyasyon pa [[User:$1|$1]].\nMotif fourni sa <em>$2</em>.",
        "filereadonlyerror": "Enposib di modifyé fiché-a « $1 » pas répèrtwar-a di fiché « $2 » sa an lèktir sèl.\n\nAdministratò sistèm ki li vérouyé té fourni sa motif : « $3 ».",
        "invalidtitle-knownnamespace": "Tit pa valid ké lèspas di non « $2 » é entitilé-a « $3 »",
        "showpreview": "Prévizwalizé",
        "showdiff": "Wè modifikasyon-yan",
        "anoneditwarning": "<strong>Panga :</strong> zòt pa konèkté. Zòt adrès IP ké sa vizib di tout moun si zòt ka fè dé modifikasyon. Si zòt <strong>[$1 ka konèkté zòt kò]</strong> ou <strong>[$2 kréyé roun kont]</strong>, zòt modifikasyon ké sq atribwé à zòt pròp non di itilizatò(ris) é zòt ké gen dé ròt avantaj.",
-       "blockedtext": "<strong>Zòt kont itilizatò ou zòt adrès IP bloké.</strong>\n\nBlokaj té éfèktchwé pa $1.\nRézon-an évoké sa swivant : <em>$2</em>.\n\n* Koumansman di blokaj : $8\n* Èkspirasyon di blokaj : $6\n* Kont bloké : $7.\n\nZòt pé kontakté $1 ou rounòt [[{{MediaWiki:Grouppage-sysop}}|administratò]] pou an diskité.\nZòt pa pé itilizé fonksyon-an « {{int:emailuser}} » sèlman si oun adrès di kouryé valid sa spésifyé andan zòt [[Special:Preferences|préférans]] é sèlman si sa fonksyonalité pa bloké.\nZòt adrèd IP atchwèl sa $3 é zòt idantifyan di blokaj sa $5.\nSouplé, enkli tout détay-ya lasou'l annan chakin dé rékèt ki zòt ké fè.",
+       "blockedtext": "<strong>Zòt kont itilizatò oben zòt adrès IP bloké.</strong>\n\nBlokaj té éfèktchwé pa $1.\nRézon-an évoké sa swivant : <em>$2</em>.\n\n* Koumansman di blokaj : $8\n* Èspirasyon di blokaj : $6\n* Kont bloké : $7.\n\nZòt pé kontakté $1 oben rounòt [[{{MediaWiki:Grouppage-sysop}}|administratò]] pou an diskité.\nZòt pa pouvé itilizé fonksyon-an « {{int:emailuser}} » rounso si oun adrès di kouryé valid sa èspésifyé andan zòt [[Special:Preferences|préférans]] é rounso si sa fonksyonalité pa bloké.\nZòt adrès IP atchwèl sa $3 é zòt idantifyan di blokaj sa $5.\nSouplé, enkli tout détay-ya lasou'l annan chakin dé rékèt ki zòt ké fè.",
        "loginreqlink": "konèkté so kò",
        "newarticletext": "Zòt té ka swiv roun lyen vèr roun paj ki pa ka ègzisté òkò. \nAtò di kréyé sa paj, antré zòt tèks annan bwat ki aprè (zòt pé konsilté [$1 paj d'èd-a] pou plis enfòrmasyon).\nSi zòt pa rivé{{GENDER:|}} isi pa éròr, kliké asou bouton <strong>Routour</strong> di zòt navigatò.",
        "anontalkpagetext": "----\n<em>Zòt asou paj di diskisyon di oun itilizatò anonim ki pa òkò kréyé di kont ou ki pa ka an itilizé</em>.\nPou sa rézon, nou divèt itilizé so adrès IP pou idantifyé li.\nOun adrès IP pé sa partajé pa plizyò itilizatò.\nSi zòt roun itiliza{{GENDER:|ò|ris}} anonim é si zòt ka kontasté ki dé koumantèr ki pa ka konsèrné zòt sa adrèsé à zòt, zòt pé [[Special:CreateAccount|kréyé roun kont]] ou [[Special:UserLogin|konèkté zòt kò]] atò di évité tout konfizyon fitir ké ròt kontribitò anonim.",
        "recentchangeslinked-feed": "Swivi dé paj lyé",
        "recentchangeslinked-toolbox": "Swivi dé paj lyé",
        "recentchangeslinked-title": "Swivi dé paj asosyé à « $1 »",
-       "recentchangeslinked-summary": "Antré roun non di paj pou wè modifikasyon-yan ki fè résaman asou dé paj lyé dipi ou vèr sa paj (pou wè manm-yan di oun katégori, antré Katégori:Non di katégori). Modifikasyon-yan dé paj di [[Special:Watchlist|zòt lis di swivi]] sa <strong>an gra</strong>.",
+       "recentchangeslinked-summary": "Antré roun non di paj pou wè modifikasyon-yan ki fè résaman asou dé paj lyé dipi oben bò'd sa paj (pou wè manm-yan di oun katégori, antré {{ns:category}}:Non di katégori). Modifikasyon-yan dé paj di [[Special:Watchlist|zòt lis di swivi]] sa <strong>an gra</strong>.",
        "recentchangeslinked-page": "Non di paj :",
        "recentchangeslinked-to": "Afiché modifikasyon-yan dé paj ki ka konpòrté roun lyen vèr paj ki bay plito ki envèrs",
        "upload": "Enpòrté roun fiché",
        "tooltip-t-contributions": "Wè lis dé kontribisyon di {{GENDER:$1|sa itilizatò|sa itilizatris}}",
        "tooltip-t-emailuser": "Voyé roun kouryé à {{GENDER:$1|sa itilizatò|sa itilizatris}}",
        "tooltip-t-upload": "Télévèrsé dé fiché",
-       "tooltip-t-specialpages": "Lis di tout paj spésyal",
+       "tooltip-t-specialpages": "Lis di tout paj èspésyal",
        "tooltip-t-print": "Vèrsyon enprimab di sa paj",
        "tooltip-t-permalink": "Adrès pèrmanant di sa vèrsyon di paj-a",
        "tooltip-ca-nstab-main": "Wè kontni di paj-a",
        "tooltip-ca-nstab-user": "Wè paj di itilizatò",
-       "tooltip-ca-nstab-special": "A roun paj spésyal, é li pa pé sa modifyé.",
+       "tooltip-ca-nstab-special": "A roun paj èspésyal, é li pa pouvé fika modifyé.",
        "tooltip-ca-nstab-project": "Wè paj-a di projè",
        "tooltip-ca-nstab-image": "Wè paj-a di fiché",
        "tooltip-ca-nstab-mediawiki": "Wè mésaj sistèm-a",
        "watchlisttools-raw": "Modifyé lis di swivi an mòd brout",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|diskisyon]])",
        "redirect": "Roudirijé pa ID di fiché, itilizatò, paj, révizyon ou journal",
-       "redirect-summary": "Sa paj spésyal ka roudirijé vèr roun fiché (non di fiché fourni), oun paj (ID di révizyon ou di paj fourni), oun paj di itilizatò (idantifyan nimérik di itilizatò fourni), ou roun antré di journal (ID di journal fourni). Itilizasyon : [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], ou [[{{#Special:Redirect}}/logid/186]].",
+       "redirect-summary": "Sa paj èspésyal ka roudirijé bò'd roun fiché (non di fiché fourni), oun paj (ID di révizyon oben di paj fourni), oun paj di itilizatò (idantifyan nimérik di itilizatò fourni), oben roun antré di journal (ID di journal fourni). Itilizasyon : [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], oben [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Validé",
        "redirect-lookup": "Sasé :",
        "redirect-value": "Valò :",
        "redirect-page": "ID di paj",
        "redirect-revision": "Révizyon di paj-a",
        "redirect-file": "Non di fiché",
-       "specialpages": "Paj spésyal",
+       "specialpages": "Paj èspésyal",
        "tag-filter": "Filtré [[Special:Tags|baliz]] :",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Baliz}}]] : $2)",
        "tags-active-yes": "Wi",
index 633d84d..4fb87e7 100644 (file)
@@ -26,7 +26,8 @@
                        "Luan",
                        "Hamilton Abreu",
                        "Athena in Wonderland",
-                       "Navhy"
+                       "Navhy",
+                       "PokéDex Nacional"
                ]
        },
        "tog-underline": "Subliñar as ligazóns:",
@@ -63,7 +64,7 @@
        "tog-watchlisthideminor": "Agochar as edicións pequenas na lista de vixilancia",
        "tog-watchlisthideliu": "Agochar as edicións dos usuarios rexistrados na lista de vixilancia",
        "tog-watchlistreloadautomatically": "Recargar a lista de vixilancia automaticamente cando se produza un cambio nun filtro (necesítase JavaScript)",
-       "tog-watchlistunwatchlinks": "Engadir ligazóns directos para vixiar ou deixar de vixiar as entradas da lista de páxinas vixiadas (é necesario JavaScript para activar a funcionalidade)",
+       "tog-watchlistunwatchlinks": "Engadir ligazóns directos para vixiar ou deixar de vixiar ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) as entradas da lista de páxinas vixiadas (é preciso JavaScript para activar a funcionalidade)",
        "tog-watchlisthideanons": "Agochar as edicións dos usuarios anónimos na lista de vixilancia",
        "tog-watchlisthidepatrolled": "Agochar as edicións patrulladas na lista de vixilancia",
        "tog-watchlisthidecategorization": "Agochar a categorización das páxinas",
        "botpasswords-existing": "Contrasinais de bots existentes",
        "botpasswords-createnew": "Crear un novo contrasinal de bot",
        "botpasswords-editexisting": "Editar un contrasinal de bot xa existente",
+       "botpasswords-label-needsreset": "(o contrasinal precisa ser redefinido)",
        "botpasswords-label-appid": "Nome do bot:",
        "botpasswords-label-create": "Crear",
        "botpasswords-label-update": "Actualizar",
index 0946de2..740ae29 100644 (file)
        "noemail": "לא רשומה כתובת דואר אלקטרוני עבור ה{{GENDER:$1|משתמש|משתמשת}} \"$1\".",
        "noemailcreate": "יש לספק כתובת דואר אלקטרוני תקינה.",
        "passwordsent": "סיסמה חדשה נשלחה לכתובת הדואר האלקטרוני הרשומה עבור \"$1\".\nאנא היכנסו חזרה לאתר אחרי שתקבלו אותה.",
-       "blocked-mailpassword": "×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a × ×\97ס×\9e×\94 ×\9eער×\99×\9b×\94. ×\9b×\93×\99 ×\9c×\9e× ×\95×¢ × ×\99צ×\95×\9c ×\9cרע×\94, ×\90×\99× ×\9a ×\9e×\95רש×\94 להשתמש באפשרות שחזור הסיסמה.",
+       "blocked-mailpassword": "×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a × ×\97ס×\9e×\94 ×\9eער×\99×\9b×\94. ×\9b×\93×\99 ×\9c×\9e× ×\95×¢ × ×\99צ×\95×\9c ×\9cרע×\94, ×\90×\99×\9f ×\91×\90פשר×\95ת×\9a להשתמש באפשרות שחזור הסיסמה.",
        "eauthentsent": "דוא\"ל אימות נשלח לכתובת הדוא\"ל שצוינה.\nלפני שדברי דוא\"ל אחרים יישלחו לחשבון הזה, יהיה {{GENDER:|עליך|עלייך}} לפעול לפי ההוראות בדוא\"ל, כדי לאשר שהחשבון אכן שייך לך.",
        "throttled-mailpassword": "כבר נשלח דוא\"ל לאיפוס הסיסמה ב{{PLURAL:$1|שעה האחרונה|שעתיים האחרונות|־$1 השעות האחרונות}}.\nכדי למנוע ניצול לרעה, יכול להישלח רק דוא\"ל אחד כזה בכל {{PLURAL:$1|שעה|שעתיים|$1 שעות}}.",
        "mailerror": "שגיאה בשליחת דוא\"ל: $1",
        "subject-preview": "תצוגה מקדימה של הנושא:",
        "previewerrortext": "אירעה שגיאה בעת הניסיון להציג תצוגה מקדימה של השינויים שלך.",
        "blockedtitle": "המשתמש חסום",
-       "blockedtext": "<strong>שם המשתמש או כתובת ה־IP שלך נחסמו.</strong>\n\nהחסימה בוצעה על ידי $1.\nהסיבה שניתנה לכך היא <em>$2</em>.\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nבאפשרותך ליצור קשר עם $1 או עם כל אחד מ[[{{MediaWiki:Grouppage-sysop}}|מפעילי המערכת]] האחרים כדי לדון בחסימה.\nאין באפשרותך להשתמש בתכונת \"שליחת דואר אלקטרוני למשתמש זה\" אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
-       "autoblockedtext": "×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a × ×\97ס×\9e×\94 ×\91×\90×\95פ×\9f ×\90×\95×\98×\95×\9e×\98×\99 ×\9b×\99×\95×\95×\9f ×©×\9eשת×\9eש ×\90×\97ר, ×©× ×\97ס×\9d ×¢×\9cÖ¾×\99×\93×\99 $1, ×\94שת×\9eש ×\91×\94.\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\9c×\97ס×\99×\9e×\94 ×\94×\99×\90:\n\n:<em>$2</em>\n\n* ×ª×\97×\99×\9cת ×\94×\97ס×\99×\9e×\94: $8\n* ×¤×§×\99עת ×\94×\97ס×\99×\9e×\94: $6\n* ×\94×\97ס×\99×\9e×\94 ×©×\91×\95צע×\94: $7\n\n×\91×\90פשר×\95ת×\9a ×\9c×\99צ×\95ר ×§×©×¨ ×¢×\9d $1 ×\90×\95 ×¢×\9d ×\9b×\9c ×\90×\97×\93 ×\9e[[{{MediaWiki:Grouppage-sysop}}|×\9eפע×\99×\9c×\99 ×\94×\9eער×\9bת]] ×\94×\90×\97ר×\99×\9d ×\9b×\93×\99 ×\9c×\93×\95×\9f ×\91×\97ס×\99×\9e×\94.\n\n×\90×\99×\9f ×\91×\90פשר×\95ת×\9a ×\9c×\94שת×\9eש ×\91ת×\9b×\95נת \"ש×\9c×\99×\97ת ×\93×\95×\90ר ×\90×\9cק×\98ר×\95× ×\99 ×\9c×\9eשת×\9eש ×\96×\94\" אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\n\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
+       "blockedtext": "<strong>שם המשתמש או כתובת ה־IP שלך נחסמו.</strong>\n\nהחסימה בוצעה על־ידי $1.\nהסיבה שניתנה לכך היא <em>$2</em>.\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nבאפשרותך ליצור קשר עם $1 או עם כל אחד מ[[{{MediaWiki:Grouppage-sysop}}|מפעילי המערכת]] האחרים כדי לדון בחסימה.\nכמו־כן, באפשרותך להשתמש בתכונת \"{{int:emailuser}}\", אלא אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
+       "autoblockedtext": "×\9bת×\95×\91ת ×\94Ö¾IP ×©×\9c×\9a × ×\97ס×\9e×\94 ×\91×\90×\95פ×\9f ×\90×\95×\98×\95×\9e×\98×\99 ×\9b×\99×\95×\95×\9f ×©×\9eשת×\9eש ×\90×\97ר, ×©× ×\97ס×\9d ×¢×\9cÖ¾×\99×\93×\99 $1, ×\94שת×\9eש ×\91×\94.\n×\94ס×\99×\91×\94 ×©× ×\99תנ×\94 ×\9c×\97ס×\99×\9e×\94 ×\94×\99×\90:\n\n:<em>$2</em>\n\n* ×ª×\97×\99×\9cת ×\94×\97ס×\99×\9e×\94: $8\n* ×¤×§×\99עת ×\94×\97ס×\99×\9e×\94: $6\n* ×\94×\97ס×\99×\9e×\94 ×©×\91×\95צע×\94: $7\n\n×\91×\90פשר×\95ת×\9a ×\9c×\99צ×\95ר ×§×©×¨ ×¢×\9d $1 ×\90×\95 ×¢×\9d ×\9b×\9c ×\90×\97×\93 ×\9e[[{{MediaWiki:Grouppage-sysop}}|×\9eפע×\99×\9c×\99 ×\94×\9eער×\9bת]] ×\94×\90×\97ר×\99×\9d ×\9b×\93×\99 ×\9c×\93×\95×\9f ×\91×\97ס×\99×\9e×\94.\n\n×\9b×\9e×\95Ö¾×\9b×\9f, ×\91×\90פשר×\95ת×\9a ×\9c×\94שת×\9eש ×\91ת×\9b×\95נת \"{{int:emailuser}}\", ×\90×\9c×\90 אם לא ציינת כתובת דוא\"ל תקפה ב[[Special:Preferences|העדפות המשתמש שלך]] או אם נחסמת משליחת דוא\"ל.\n\nכתובת ה־IP הנוכחית שלך היא $3, ומספר החסימה שלך הוא #$5.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "systemblockedtext": "שם המשתמש או כתובת ה־IP שלך נחסמו באופן אוטומטי על־ידי תוכנת מדיה־ויקי.\nהסיבה שניתנה לחסימה היא:\n\n:<em>$2</em>\n\n* תחילת החסימה: $8\n* פקיעת החסימה: $6\n* החסימה שבוצעה: $7\n\nכתובת ה־IP הנוכחית שלך היא $3.\nיש לציין את כל הפרטים הללו בכל פנייה לבירור החסימה.",
        "blockednoreason": "לא ניתנה סיבה",
        "whitelistedittext": "נדרשת $1 כדי לערוך דפים.",
        "listduplicatedfiles-summary": "זוהי רשימה של קבצים שהגרסה החדשה ביותר שלהם זהה לגרסה החדשה ביותר של קובץ אחר כלשהו. רק קבצים מקומיים נבדקים לצורך זה.",
        "listduplicatedfiles-entry": "לקובץ [[:File:$1|$1]] יש [[$3|{{PLURAL:$2|עותק זהה|$2 עותקים זהים}}]].",
        "unusedtemplates": "תבניות שאינן בשימוש",
-       "unusedtemplatestext": "דף זה מכיל רשימה של כל הדפים במרחב השם {{ns:template}} שאינם נכללים בדף אחר. אנא זכרו לבדוק את הקישורים האחרים לתבניות לפני שתמחקו אותן.",
+       "unusedtemplatestext": "דף זה מכיל רשימה של כל הדפים במרחב השם \"{{ns:template}}\" שאינם נכללים בדף אחר.\nיש לזכור לבדוק את הקישורים האחרים לתבניות לפני מחיקתן.",
        "unusedtemplateswlh": "קישורים אחרים",
        "randompage": "דף אקראי",
        "randompage-nopages": "אין דפים {{PLURAL:$2|במרחב השם הבא|במרחבי השם הבאים}}: $1.",
        "brokenredirects-edit": "עריכה",
        "brokenredirects-delete": "מחיקה",
        "withoutinterwiki": "דפים ללא קישורי שפה",
-       "withoutinterwiki-summary": "הדפים הבאים אינם מקשרים לגרסאות שלהם בשפות אחרות:",
+       "withoutinterwiki-summary": "הדפים הבאים אינם מקשרים לגרסאות שלהם בשפות אחרות.",
        "withoutinterwiki-legend": "תחילית",
        "withoutinterwiki-submit": "הצגה",
        "fewestrevisions": "הדפים בעלי מספר העריכות הנמוך ביותר",
        "nbytes": "{{PLURAL:$1|בית אחד|$1 בתים}}",
        "ncategories": "{{PLURAL:$1|קטגוריה אחת|$1 קטגוריות}}",
-       "ninterwikis": "{{PLURAL:$1|ק×\99ש×\95ר ×\91×\99× ×\95×\95×\99ק×\99 ×§חד|$1 קישורי בינוויקי}}",
+       "ninterwikis": "{{PLURAL:$1|ק×\99ש×\95ר ×\91×\99× ×\95×\95×\99ק×\99 ×\90חד|$1 קישורי בינוויקי}}",
        "nlinks": "{{PLURAL:$1|קישור אחד|$1 קישורים}}",
        "nmembers": "{{PLURAL:$1|דף אחד|$1 דפים}}",
        "nmemberschanged": "$1 ← {{PLURAL:$2|חבר אחד|$2 חברים}}",
        "nrevisions": "{{PLURAL:$1|גרסה אחת|$1 גרסאות}}",
        "nimagelinks": "בשימוש {{PLURAL:$1|בדף אחד|ב־$1 דפים}}",
        "ntransclusions": "בשימוש {{PLURAL:$1|בדף אחד|ב־$1 דפים}}",
-       "specialpage-empty": "אין תוצאות.",
+       "specialpage-empty": "אין תוצאות בדיווח התחזוקה הזה.",
        "lonelypages": "דפים יתומים",
-       "lonelypagestext": "×\94×\93פ×\99×\9d ×\94×\91×\90×\99×\9d ×\90×\99× ×\9d ×\9eק×\95שר×\99×\9d ×\95×\90×\99× ×\9d ×\9e×\95×\9b×\9c×\9c×\99×\9d ×\91×\93פ×\99×\9d ×\90×\97ר×\99×\9d ×\91×\90תר {{SITENAME}}.",
+       "lonelypagestext": "×\94×\93פ×\99×\9d ×\94×\91×\90×\99×\9d ×\90×\99× ×\9d ×\9eק×\95שר×\99×\9d ×\9e×\93פ×\99×\9d ×\90×\97ר×\99×\9d ×\91{{GRAMMAR:ת×\97×\99×\9c×\99ת|{{SITENAME}}}} ×\95×\90×\99× ×\9d ×\9e×\95×\9b×\9c×\9c×\99×\9d ×\91×\94×\9d.",
        "uncategorizedpages": "דפים חסרי קטגוריה",
        "uncategorizedcategories": "קטגוריות חסרות קטגוריה",
        "uncategorizedimages": "קבצים חסרי קטגוריה",
        "unusedimages": "קבצים שאינם בשימוש",
        "wantedcategories": "קטגוריות מבוקשות",
        "wantedpages": "דפים מבוקשים",
-       "wantedpages-summary": "רש×\99×\9eת ×\93פ×\99×\9d ×\9c×\90 ×§×\99×\99×\9e×\99×\9d ×©×\9eספר ×\94ק×\99ש×\95ר×\99×\9d ×\90×\9c×\99×\94×\9d ×\94×\95×\90 ×\94×\92×\93×\95×\9c ×\91×\99×\95תר, ×\9c×\9e×¢×\98 ×\93פ×\99×\9d ×©×¨×§ ×\94פנ×\99×\95ת ×\9eקשר×\95ת ×\90×\9c×\99×\94×\9d. ×\9cרש×\99×\9eת ×\93פ×\99×\9d ×\9c×\90 ×§×\99×\99×\9e×\99×\9d ×©×\99ש ×\94פנ×\99×\95ת ×\94×\9eקשר×\95ת ×\90×\9c×\99×\94×\9d, ×¨' [[{{#special:BrokenRedirects}}|רשימת ההפניות הבלתי־תקינות]].",
+       "wantedpages-summary": "רש×\99×\9eת ×\93פ×\99×\9d ×\9c×\90 ×§×\99×\99×\9e×\99×\9d ×©×\9eספר ×\94ק×\99ש×\95ר×\99×\9d ×\90×\9c×\99×\94×\9d ×\94×\95×\90 ×\94×\92×\93×\95×\9c ×\91×\99×\95תר, ×\9c×\9e×¢×\98 ×\93פ×\99×\9d ×©×¨×§ ×\94פנ×\99×\95ת ×\9eקשר×\95ת ×\90×\9c×\99×\94×\9d. ×\9cרש×\99×\9eת ×\93פ×\99×\9d ×\9c×\90 ×§×\99×\99×\9e×\99×\9d ×©×\99ש ×\94פנ×\99×\95ת ×\94×\9eקשר×\95ת ×\90×\9c×\99×\94×\9d, × ×\99ת×\9f ×\9c×¢×\99×\99×\9f ×\91[[{{#special:BrokenRedirects}}|רשימת ההפניות הבלתי־תקינות]].",
        "wantedpages-badtitle": "כותרת בלתי תקינה ברשימת התוצאות: $1",
        "wantedfiles": "קבצים מבוקשים",
        "wantedfiletext-cat": "הקבצים הבאים נמצאים בשימוש, אך אינם קיימים. ייתכן שקבצים ממאגרים חיצוניים יהיו רשומים אף על פי שהם קיימים, אך שגיאות כאלה יהיו <del>מחוקות</del>. בנוסף, דפים שמטביעים קבצים שאינם קיימים רשומים בדף [[:$1]].",
-       "wantedfiletext-cat-noforeign": "×\94ק×\91צ×\99×\9d ×\94×\91×\90×\99×\9d × ×\9eצ×\90×\99×\9d ×\91ש×\99×\9e×\95ש, ×\90×\91×\9c ×\90×\99× ×\9d ×§×\99×\99×\9e×\99×\9d. ×\9b×\9e×\95Ö¾×\9b×\9f, ×\93פ×\99×\9d ×©×\9eשת×\9eש×\99×\9d ×\91קבצים שאינם קיימים רשומים בדף [[:$1]].",
+       "wantedfiletext-cat-noforeign": "×\94ק×\91צ×\99×\9d ×\94×\91×\90×\99×\9d × ×\9eצ×\90×\99×\9d ×\91ש×\99×\9e×\95ש, ×\90×\91×\9c ×\90×\99× ×\9d ×§×\99×\99×\9e×\99×\9d. ×\9b×\9e×\95Ö¾×\9b×\9f, ×\93פ×\99×\9d ×©×\9e×\98×\91×\99×¢×\99×\9d קבצים שאינם קיימים רשומים בדף [[:$1]].",
        "wantedfiletext-nocat": "הקבצים הבאים נמצאים בשימוש, אך אינם קיימים. ייתכן שקבצים ממאגרים חיצוניים יהיו רשומים אף על פי שהם קיימים, אך שגיאות כאלה יהיו <del>מחוקות</del>.",
        "wantedfiletext-nocat-noforeign": "הקבצים הבאים נמצאים בשימוש, אבל אינם קיימים.",
        "wantedtemplates": "תבניות מבוקשות",
        "mostinterwikis": "הדפים עם המספר הרב ביותר של קישורי בינוויקי",
        "mostrevisions": "הדפים עם מספר העריכות הגבוה ביותר",
        "prefixindex": "כל הדפים עם התחילית",
-       "prefixindex-namespace": "כל הדפים עם התחילית (במרחב השם $1)",
+       "prefixindex-namespace": "כל הדפים עם התחילית (במרחב השם \"$1\")",
        "prefixindex-submit": "הצגה",
        "prefixindex-strip": "הסתרת התחילית ברשימה",
        "shortpages": "דפים קצרים",
        "deadendpages": "דפים ללא קישורים",
        "deadendpagestext": "הדפים הבאים אינם מקשרים לדפים אחרים באתר {{SITENAME}}.",
        "protectedpages": "דפים מוגנים",
-       "protectedpages-filters": "×\9eסננ×\99×\9d:",
+       "protectedpages-filters": "ס×\99× ×\95×\9f:",
        "protectedpages-indef": "הגנות ללא הגבלת זמן בלבד",
-       "protectedpages-summary": "×\91×\93×£ ×\96×\94 ×¨×©×\95×\9e×\99×\9d ×\94×\93פ×\99×\9d ×\94ק×\99×\99×\9e×\99×\9d ×©×\9e×\95×\92× ×\99×\9d ×\9bר×\92×¢. ×\9cרש×\99×\9eת ×\94×\9b×\95תר×\95ת ×©×\9e×\95×\92× ×\95ת ×\9eפנ×\99 ×\99צ×\99ר×\94, ×¨×\90×\95 [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
+       "protectedpages-summary": "×\91×\93×£ ×\96×\94 ×¨×©×\95×\9e×\99×\9d ×\94×\93פ×\99×\9d ×\94ק×\99×\99×\9e×\99×\9d ×©×\9e×\95×\92× ×\99×\9d ×\9bר×\92×¢. ×¨×©×\99×\9eת ×\94×\9b×\95תר×\95ת ×©×\9e×\95×\92× ×\95ת ×\9eפנ×\99 ×\99צ×\99ר×\94 ×\9e×\95פ×\99×¢×\94 ×\91×\93×£ [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]].",
        "protectedpages-cascade": "הגנות מדורגות בלבד",
        "protectedpages-noredirect": "הסתרת הפניות",
        "protectedpagesempty": "אין כרגע דפים מוגנים עם הפרמטרים הללו.",
        "protectedpages-unknown-timestamp": "לא ידוע",
        "protectedpages-unknown-performer": "משתמש לא ידוע",
        "protectedtitles": "כותרות מוגנות",
-       "protectedtitles-summary": "×\91×\93×£ ×\96×\94 ×¨×©×\95×\9e×\95ת ×\94×\9b×\95תר×\95ת ×©×\9c ×\94×\93פ×\99×\9d ×©×\9e×\95×\92× ×\99×\9d ×\9bעת ×\9eפנ×\99 ×\99צ×\99ר×\94. ×\9cרש×\99×\9eת ×\94×\93פ×\99×\9d ×\94ק×\99×\99×\9e×\99×\9d ×©×\9e×\95×\92× ×\99×\9d, {{GENDER:|ר×\90×\94|ר×\90×\99|ר×\90×\95}} [[{{#special:ProtectedPages}}|{{int:protectedpages}}]].",
+       "protectedtitles-summary": "×\91×\93×£ ×\96×\94 ×¨×©×\95×\9e×\95ת ×\94×\9b×\95תר×\95ת ×©×\9c ×\94×\93פ×\99×\9d ×©×\9e×\95×\92× ×\99×\9d ×\9bעת ×\9eפנ×\99 ×\99צ×\99ר×\94. ×¨×©×\99×\9eת ×\94×\93פ×\99×\9d ×\94ק×\99×\99×\9e×\99×\9d ×©×\9e×\95×\92× ×\99×\9d ×\9e×\95פ×\99×¢×\94 ×\91×\93×£ [[{{#special:ProtectedPages}}|{{int:protectedpages}}]].",
        "protectedtitlesempty": "אין כרגע כותרות מוגנות עם הפרמטרים האלה.",
        "protectedtitles-submit": "הצגת הדפים",
        "listusers": "רשימת משתמשים",
        "ancientpages": "דפים מוזנחים",
        "move": "העברה",
        "movethispage": "העברת דף זה",
-       "unusedimagestext": "הקבצים הבאים קיימים אך אינם מוטבעים בשום דף.\nשימו לב שאתרי אינטרנט אחרים עשויים לקשר לקובץ באמצעות כתובת URL ישירה, ולכן הוא עלול להופיע כאן למרות היותו בשימוש פעיל.",
+       "unusedimagestext": "הקבצים הבאים קיימים, אך אינם מוטבעים בשום דף.\nיש לשים לב לכך שאתרי אינטרנט אחרים עשויים לקשר לקובץ באמצעות כתובת URL ישירה, ולכן הוא עלול להופיע כאן למרות היותו בשימוש פעיל.",
        "unusedcategoriestext": "הקטגוריות הבאות קיימות, אבל לא נעשה שימוש בהן בשום דף או קטגוריה.",
        "notargettitle": "אין דף מטרה",
        "notargettext": "לא ציינת דף מטרה או משתמש לגביו תבוצע פעולה זו.",
        "nopagetext": "דף המטרה שציינת אינו קיים.",
        "pager-newer-n": "{{PLURAL:$1|הבאה|$1 הבאות}}",
        "pager-older-n": "{{PLURAL:$1|הקודמת|$1 הקודמות}}",
-       "suppress": "×\94סתרה",
+       "suppress": "×\94×¢×\9c×\9eה",
        "querypage-disabled": "דף מיוחד זה מבוטל עקב בעיות ביצועים.",
        "apihelp": "עזרה עבור ה־API",
        "apihelp-no-such-module": "המודול \"$1\" לא נמצא.",
        "apisandbox": "ארגז החול של ה־API",
        "apisandbox-jsonly": "דרוש JavaScript כדי להשתמש בארגז החול של ה־API.",
        "apisandbox-api-disabled": "API אינו פעיל באתר הזה.",
-       "apisandbox-intro": "×\94שת×\9eש×\95 ×\91×\93×£ ×\94×\96×\94 ×\9b×\93×\99 ×\9c×\94תנס×\95ת ×\91ש×\99×\9e×\95ש ×\91<strong>ש×\99ר×\95ת ×\94Ö¾API ×\94×\9e×\91×\95סס Web ×©×\9c ×\9e×\93×\99×\94Ö¾×\95×\99ק×\99</strong>.\n×¢×\99×\99× ×\95 ×\91[[mw:API:Main page|ת×\99×¢×\95×\93 ×©×\9c ×\94Ö¾API]] (×\91×\90× ×\92×\9c×\99ת) ×\9c×\9e×\99×\93×¢ × ×\95סף ×©×\9c ×©×\99×\9e×\95ש ×\91Ö¾API. ×\9c×\9eש×\9c: [https://www.mediawiki.org/wiki/API#A_simple_example ×\90×\99×\9a ×\9cק×\91×\9c ×\90ת ×\94ת×\95×\9b×\9f ×©×\9c ×\94×¢×\9e×\95×\93 ×\94ר×\90ש×\99]. ×\91×\97ר×\95 ×\91×\90×\97ת ×\94פע×\95×\9c×\95ת (actions) ×\9c×\93×\95×\92×\9e×\90×\95ת × ×\95ספ×\95ת.\n\nש×\99×\9e×\95 ×\9c×\91 ×©×\90×£ שמדובר ב\"ארגז חול\", פעולות שנעשות כאן עשויות לשנות את התוכן של אתר הוויקי.",
+       "apisandbox-intro": "× ×\99ת×\9f ×\9c×\94שת×\9eש ×\91×\93×£ ×\94×\96×\94 ×\9b×\93×\99 ×\9c×\94תנס×\95ת ×\91ש×\99×\9e×\95ש ×\91<strong>ש×\99ר×\95ת ×\94Ö¾API ×\94×\9e×\91×\95סס Web ×©×\9c ×\9e×\93×\99×\94Ö¾×\95×\99ק×\99</strong>.\n×\90פשר ×\9c×¢×\99×\99×\9f ×\91[[mw:API:Main page|ת×\99×¢×\95×\93 ×©×\9c ×\94Ö¾API]] (×\91×\90× ×\92×\9c×\99ת) ×\9c×\9e×\99×\93×¢ × ×\95סף ×¢×\9c ×©×\99×\9e×\95ש ×\91Ö¾API. ×\9c×\9eש×\9c: [https://www.mediawiki.org/wiki/API#A_simple_example ×\90×\99×\9a ×\9cק×\91×\9c ×\90ת ×\94ת×\95×\9b×\9f ×©×\9c ×\94×¢×\9e×\95×\93 ×\94ר×\90ש×\99]. ×\99ש ×\9c×\91×\97×\95ר ×\91×\90×\97ת ×\94פע×\95×\9c×\95ת (actions) ×\9c×\93×\95×\92×\9e×\90×\95ת × ×\95ספ×\95ת.\n\n×\9cתש×\95×\9eת ×\9c×\91×\9a: ×\90×£ ×¢×\9c ×¤×\99 שמדובר ב\"ארגז חול\", פעולות שנעשות כאן עשויות לשנות את התוכן של אתר הוויקי.",
        "apisandbox-submit": "ביצוע הבקשה",
        "apisandbox-reset": "ניקוי",
        "apisandbox-retry": "ניסיון נוסף",
        "apisandbox-fetch-token": "מילוי אוטומטי של האסימון",
        "apisandbox-add-multi": "הוספה",
        "apisandbox-submit-invalid-fields-title": "חלק מהשדות אינם תקינים",
-       "apisandbox-submit-invalid-fields-message": "×\90× ×\90 ×ª×§× ×\95 ×\90ת ×\94ש×\93×\95ת ×\94×\9eס×\95×\9e× ×\99×\9d ×\95נס×\95 שוב.",
+       "apisandbox-submit-invalid-fields-message": "× ×\90 ×\9cתק×\9f ×\90ת ×\94ש×\93×\95ת ×\94×\9eס×\95×\9e× ×\99×\9d ×\95×\9cנס×\95ת שוב.",
        "apisandbox-results": "תוצאות",
        "apisandbox-sending-request": "בקשת ה־API בשליחה...",
        "apisandbox-loading-results": "תוצאות ה־API בתהליך קבלה...",
        "apisandbox-request-url-label": "כתובת ה־URL של הבקשה:",
        "apisandbox-request-json-label": "ייצוג הבקשה כ־JSON:",
        "apisandbox-request-time": "זמן הבקשה: {{PLURAL:$1|מילישנייה אחת|$1 מילישניות}}",
-       "apisandbox-results-fixtoken": "×\90× ×\90 ×ª×§× ×\95 ×\90ת ×\94×\90ס×\99×\9e×\95×\9f ×\95ש×\9c×\97×\95 שוב",
+       "apisandbox-results-fixtoken": "×\99ש ×\9cתק×\9f ×\90ת ×\94×\90ס×\99×\9e×\95×\9f ×\95×\9cש×\9c×\95×\97 שוב",
        "apisandbox-results-fixtoken-fail": "קבלת האסימון \"$1\" נכשלה.",
        "apisandbox-alert-page": "שדות בדף זה אינם תקינים.",
        "apisandbox-alert-field": "הערך של שדה זה אינו תקין.",
        "booksources-search-legend": "חיפוש משאבי ספרות חיצוניים",
        "booksources-isbn": "מסת\"ב (ISBN):",
        "booksources-search": "חיפוש",
-       "booksources-text": "×\9c×\94×\9c×\9f ×¨×©×\99×\9eת ×§×\99ש×\95ר×\99×\9d ×\9c×\90תר×\99×\9d ×\90×\97ר×\99×\9d ×\94×\9e×\95×\9bר×\99×\9d ×¡×¤×¨×\99×\9d ×\97×\93ש×\99×\9d ×\95×\99×\93־שנ×\99×\99×\94, ×\95ש×\91×\94×\9d ×¢×©×\95×\99 ×\9c×\94×\99×\95ת ×\9e×\99×\93×¢ × ×\95סף ×\9c×\92×\91×\99 ×¡×¤×¨×\99×\9d ×©×\90ת×\9d ×\9e×\97פש×\99×\9d:",
-       "booksources-invalid-isbn": "×\94×\9eסת\"×\91 ×©× ×\99ת×\9f ×\9bנר×\90×\94 ×\90×\99× ×\95 ×ª×§×\99×\9f; ×\90× ×\90 ×\91×\93ק×\95 ×\90×\9d ×\91×\99צעת×\9d טעויות בהעתקה מהמידע המקורי.",
+       "booksources-text": "×\9c×\94×\9c×\9f ×¨×©×\99×\9eת ×§×\99ש×\95ר×\99×\9d ×\9c×\90תר×\99×\9d ×\90×\97ר×\99×\9d ×\94×\9e×\95×\9bר×\99×\9d ×¡×¤×¨×\99×\9d ×\97×\93ש×\99×\9d ×\95×\99×\93־שנ×\99×\99×\94, ×\95ש×\91×\94×\9d ×¢×©×\95×\99 ×\9c×\94×\99×\95ת ×\9e×\99×\93×¢ × ×\95סף ×\9c×\92×\91×\99 ×¡×¤×¨×\99×\9d ×©×\9e×¢× ×\99×\99× ×\99×\9d ×\90×\95ת×\9a:",
+       "booksources-invalid-isbn": "×\94×\9eסת\"×\91 ×©× ×\99ת×\9f ×\9bנר×\90×\94 ×\90×\99× ×\95 ×ª×§×\99×\9f; ×\99ש ×\9c×\91×\93×\95ק ×\90×\9d × ×¢×©×\95 טעויות בהעתקה מהמידע המקורי.",
        "magiclink-tracking-rfc": "דפים שמשתמשים בקישורי קסם ל־RFC",
        "magiclink-tracking-rfc-desc": "דף זה משתמש בקישורי קסם ל־RFC. באתר [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Magic_links mediawiki.org] מוסבר כיצד יש לשנותם.",
        "magiclink-tracking-pmid": "דפים שמשתמשים בקישורי קסם ל־PMID",
index f82859f..53241d4 100644 (file)
        "longpageerror": "'''HIBA: Az általad beküldött szöveg {{PLURAL:$1|egy kilobájt|$1 kilobájt}} hosszú, ami több az engedélyezett {{PLURAL:$2|egy kilobájtnál|$2 kilobájtnál}}.\nA szerkesztést nem lehet elmenteni.'''",
        "readonlywarning": "<strong>FIGYELMEZTETÉS: A wiki adatbázisát karbantartás miatt zárolták, ezért most nem fogod tudni elmenteni a szerkesztéseidet!</strong>\nA lap szövegét másold egy szövegfájlba, amit később felhasználhatsz!\n\nAz adatbázist lezáró rendszeradminisztrátor az alábbi magyarázatot adta: $1",
        "protectedpagewarning": "<strong>Figyelem: Ez a lap védett, így csak adminisztrátori jogosultságokkal rendelkező szerkesztők módosíthatják.</strong>\nA legutolsó ide vonatkozó naplóbejegyzés alább látható:",
-       "semiprotectedpagewarning": "'''Megjegyzés:''' ez a lap védett, így regisztrálatlan vagy újonnan regisztrált szerkesztők nem módosíthatják.",
+       "semiprotectedpagewarning": "<strong>Megjegyzés:</strong> ez a lap védett, így regisztrálatlan vagy újonnan regisztrált szerkesztők nem módosíthatják.\nA lapra vonatkozó utolsó naplóbejegyzés alább látható:",
        "cascadeprotectedwarning": "<strong>Figyelem:</strong> ez a lap le van zárva, csak [[Special:ListGroupRights|megfelelő jogosultságú]] felhasználók szerkeszthetik, mert a következő kaszkádvédelemmel ellátott {{PLURAL:$1|lapon|lapokon}} be van illesztve:",
        "titleprotectedwarning": "'''Figyelem: Ez a lap le van védve, így csak a [[Special:ListGroupRights|megfelelő jogosultságokkal]] rendelkező szerkesztők hozhatják létre.'''\nA legutolsó ide vonatkozó naplóbejegyzés alább látható:",
        "templatesused": "A lapon használt {{PLURAL:$1|sablon|sablonok}}:",
index 3efb8ca..3bd7b9e 100644 (file)
@@ -54,7 +54,7 @@
        "tog-watchlisthideminor": "Celar modificationes minor in le observatorio",
        "tog-watchlisthideliu": "Celar modificationes de usatores registrate in le observatorio",
        "tog-watchlistreloadautomatically": "Recargar automaticamente le observatorio quando un filtro es cambiate (JavaScript requirite)",
-       "tog-watchlistunwatchlinks": "Adjunger ligamines directe pro disobservar/observar al entratas del observatorio (JavaScript es necessari pro le functionalitate de alternar)",
+       "tog-watchlistunwatchlinks": "Adjunger marcatores directe pro disobservar/observar ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) al paginas sub observation que ha cambiate (JavaScript es necessari pro le functionalitate de alternar)",
        "tog-watchlisthideanons": "Celar modificationes de usatores anonyme in le observatorio",
        "tog-watchlisthidepatrolled": "Celar le modificationes patruliate in le observatorio",
        "tog-watchlisthidecategorization": "Celar le categorisation de paginas",
        "cascadeprotected": "Iste pagina ha essite protegite contra modificationes perque illo es transcludite in le sequente {{PLURAL:$1|pagina, le qual|paginas, le quales}} es protegite usante le option \"cascada\":\n$2",
        "namespaceprotected": "Tu non ha le permission de modificar paginas in le spatio de nomines '''$1'''.",
        "customcssprotected": "Tu non ha le permission de modificar iste pagina de CSS perque illo contine le configuration personal de un altere usator.",
+       "customjsonprotected": "Tu non ha le permission de modificar iste pagina JSON perque illo contine le configuration personal de un altere usator.",
        "customjsprotected": "Tu non ha le permission de modificar iste pagina de JavaScript perque illo contine le configuration personal de un altere usator.",
        "mycustomcssprotected": "Tu non ha le permission de modificar iste pagina de CSS.",
+       "mycustomjsonprotected": "Tu non ha le permission de modificar iste pagina JSON.",
        "mycustomjsprotected": "Tu non ha le permission de modificar iste pagina de JavaScript.",
        "myprivateinfoprotected": "Tu non ha le permission de modificar le proprie information private.",
        "mypreferencesprotected": "Tu non ha le permission de modificar le proprie preferentias.",
        "wrongpasswordempty": "Tu non entrava un contrasigno. Per favor reprova.",
        "passwordtooshort": "Le contrasignos debe continer al minus {{PLURAL:$1|1 character|$1 characteres}}.",
        "passwordtoolong": "Le contrasignos non pote esser plus longe de {{PLURAL:$1|1 character|$1 characteres}}.",
-       "passwordtoopopular": "Contrasignos habitual non pote esser usate. Per favor, elige un contrasigno plus unic.",
+       "passwordtoopopular": "Contrasignos habitual non pote esser usate. Per favor, elige un contrasigno plus difficile a divinar.",
        "password-name-match": "Tu contrasigno debe esser differente de tu nomine de usator.",
        "password-login-forbidden": "Le uso de iste nomine de usator e contrasigno ha essite prohibite.",
        "mailmypassword": "Reinitialisar contrasigno",
        "passwordremindertitle": "Nove contrasigno temporari pro {{SITENAME}}",
-       "passwordremindertext": "Alcuno (probabilemente tu, ab le adresse IP $1) requestava un nove\ncontrasigno pro {{SITENAME}} ($4).\nUn contrasigno temporari pro le usator \"$2\" ha essite create; iste\ncontrasigno es \"$3\". Si isto esseva le intention, tu debe ora\naperir un session e eliger un nove contrasigno. Le contrasigno temporari\nexpirara post {{PLURAL:$5|un die|$5 dies}}.\n\nSi un altere persona ha facite iste requesta, o si tu te ha rememorate\nle contrasigno e non plus vole cambiar lo, tu pote ignorar iste message\ne continuar a usar le contrasigno original.",
+       "passwordremindertext": "Alcuno (ab le adresse IP $1) requestava un nove\ncontrasigno pro {{SITENAME}} ($4).\nUn contrasigno temporari pro le usator \"$2\" ha essite create; iste\ncontrasigno es \"$3\". Si isto esseva le intention, tu debe ora\naperir un session e eliger un nove contrasigno. Le contrasigno temporari\nexpirara post {{PLURAL:$5|un die|$5 dies}}.\n\nSi un altere persona ha facite iste requesta, o si tu te ha rememorate\nle contrasigno e non plus vole cambiar lo, tu pote ignorar iste message\ne continuar a usar le contrasigno original.",
        "noemail": "Il non ha un adresse de e-mail registrate pro le usator \"$1\".",
        "noemailcreate": "Es necessari fornir un adresse de e-mail valide",
        "passwordsent": "Un nove contrasigno ha essite inviate al adresse de e-mail registrate pro \"$1\".\nPer favor aperi session de novo post reciper lo.",
        "botpasswords-existing": "Contrasignos de robot existente",
        "botpasswords-createnew": "Crear un nove contrasigno de robot",
        "botpasswords-editexisting": "Modificar un contrasigno de robot existente",
+       "botpasswords-label-needsreset": "(contrasigno debe esser reinitialisate)",
        "botpasswords-label-appid": "Nomine del robot:",
        "botpasswords-label-create": "Crear",
        "botpasswords-label-update": "Actualisar",
        "botpasswords-restriction-failed": "Session impedite per restrictiones de contrasigno de robot.",
        "botpasswords-invalid-name": "Iste nomine de usator non contine le separator pro contrasigno de robot (\"$1\").",
        "botpasswords-not-exist": "Le usator \"$1\" non ha un contrasigno de robot del nomine \"$2\".",
+       "botpasswords-needs-reset": "Le contrasigno pro le robot \"$2\" del {{GENDER:$1|usator}} \"$1\" debe esser reinitialisate.",
        "resetpass_forbidden": "Le contrasignos non pote esser cambiate",
        "resetpass_forbidden-reason": "Le contrasignos non pote esser cambiate: $1",
        "resetpass-no-info": "Tu debe aperir un session pro poter acceder directemente a iste pagina.",
        "savechanges": "Salveguardar modificationes",
        "publishpage": "Publicar pagina",
        "publishchanges": "Publicar modificationes",
+       "savearticle-start": "Salveguardar pagina…",
+       "savechanges-start": "Salveguardar modificationes…",
+       "publishpage-start": "Publicar pagina…",
+       "publishchanges-start": "Publicar modificationes…",
        "preview": "Previsualisation",
        "showpreview": "Monstrar previsualisation",
        "showdiff": "Detaliar modificationes",
index d652bd1..2a1d632 100644 (file)
        "history": "Ịta ihüá",
        "history_short": "Ịta",
        "updatedmarker": "ihe gáráníru ké mgbe m byàrà nga mbu",
-       "printableversion": "Ùdì ǹke mbifụ̀",
+       "printableversion": "Ùdì ǹke mbipụ̀",
        "permalink": "Jikodo ekechịrị",
        "print": "Dotié",
        "view": "Lèzí",
        "logout": "Fwuör",
        "userlogout": "Fwuör",
        "notloggedin": "I bátà bò",
+       "userlogin-joinproject": "Bàkọ {{SITENAME}}",
        "createaccount": "Ké otụ buwa",
        "createaccountmail": "na e-mail",
+       "createacct-benefit-heading": "{{SITENAME}} sì nà aka ndị dị kà gị.",
        "createacct-benefit-body1": "{{PLURAL:$1|ḿmezi}}",
        "badretype": "Mkpurụ okwu ejị a gafẹ é jëghị.",
        "userexists": "Áhè ọ'bànifé tírí di na áká onye ozor.\nBíkó nwèré áhà nke ozor.",
        "pageinfo-header-edits": "Mèzí ịta",
        "pageinfo-length": "Ogologo ihü (na baitusu)",
        "pageinfo-article-id": "ID Ihü",
+       "pageinfo-toolboxlink": "Nkàta ihu",
        "pageinfo-redirectsto-info": "ọ́márí",
        "pageinfo-contentpage-yes": "Eeh",
        "pageinfo-protect-cascading-yes": "Eeh",
        "special-characters-group-latin": "Latin",
        "special-characters-group-latinextended": "Latin dọsàrà",
        "special-characters-group-ipa": "IPA",
-       "special-characters-group-symbols": "Nkárí",
+       "special-characters-group-symbols": "Akàrà",
        "special-characters-group-greek": "Greek",
        "special-characters-group-cyrillic": "Cyrillic",
        "special-characters-group-arabic": "Arabiki",
index 80cf726..110c031 100644 (file)
        "userpage-userdoesnotexist-view": "«$1» яха дагара йоазув долаш дац.",
        "clearyourcache": "<strong>Теркал де.</strong> Хетаргахьа, оагIув дIаязъяь яьлча шоай браузера кэш IоцIанъе езаргья шун, даь хувцамаш гургдолаш.\n* <strong>Firefox / Safari:</strong> <em>Shift</em> яха лак тоIояь лоаттаеш инструментий цхьа дакъа тIа тоIае <em>Обновить</em> е <em>Ctrl-F5</em> тоIае е <em>Ctrl-R</em> (<em>⌘-R</em> Mac тIа)\n* <strong>Google Chrome:</strong> ТоIае <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> Mac тIа)\n* <strong>Internet Explorer:</strong> <em>Ctrl</em> яха лак тоIояь лоаттаеш, тоIае <em>Обновить</em> е <em>Ctrl-F5</em> тоIае\n* <strong>Opera:</strong> ДехьагIо <em>Menu → Настройки</em> (<em>Opera → Настройки</em> Mac тIа), тIаккха <em>Безопасность → Очистить историю посещений → Кэшированные изображения и файлы</em>",
        "note": "'''Белгалдоахар:'''",
-       "previewnote": "'''Теркам бе, ер хьалххе бIаргтохар мара бац.'''\nХьа хувцамаш хIанза а дIаяздаь дац!",
-       "continue-editing": "Хувцар кхы дIахо де",
+       "previewnote": "'''Теркам бе! Ер хьалххе бIаргтохар мара бац.'''\nХьа хувцамаш хIанзехьа кхы а дIаяздаь дац!",
+       "continue-editing": "Хувцам бергболаш дIахо дехьавáла",
        "editing": "Хувцам: $1",
-       "creating": "«$1» оагIув хьакхоллар",
-       "editingsection": "Хувцам: $1 (оагӀон дáкъа)",
+       "creating": "«$1» яха оагIув хьакхоллар",
+       "editingsection": "Хувцар: $1 (оагӀон дáкъа)",
        "editingcomment": "Хувцам: $1 (оагӀон керда дáкъа)",
        "editconflict": "Хувцама вIашдухьалъоттам: $1",
        "yourtext": "Хьа текст",
index 8032223..e862694 100644 (file)
        "rev-deleted-user-contribs": "[Uzero od IP-adreso eliminita - la redakto celesis de la kontributaji]",
        "rev-delundel": "montrar/celar",
        "rev-showdeleted": "montrar",
+       "revisiondelete": "Efacar/Restaurar revizi",
        "revdelete-show-file-submit": "Yes",
        "revdelete-hide-image": "Celar kontenajo dil arkivo",
        "revdelete-hide-comment": "Rezumo di redakto",
        "right-upload": "Adkargar arkivi",
        "right-writeapi": "Uzez API por skribar",
        "right-delete": "Efacar pagini",
+       "right-deleterevision": "Efacar e restaurar specifika revizi de la pagini",
        "right-browsearchive": "Serchar pagini efacita",
        "right-suppressrevision": "Vidar, celar e deskovrar specifika revizi di pagini de irga uzero",
        "right-blockemail": "Blokusar uzero pri sendar e-posto",
        "restriction-upload": "Adkargar",
        "undelete": "Vidar efacita pagini",
        "undeletepage": "Vidar e restaurar efacita pagini",
+       "undeletepagetitle": "<strong>Yen la efacita versioni di la pagino [[:$1|$1]]</strong>.",
+       "viewdeletedpage": "Vidar pagini efacita",
        "undeletepagetext": "La sequanta {{PLURAL:$1|pagino|pagini}} efacesis ma {{PLURAL:$1|ol|li}} ankore esas en la arkivo ed esas restaurebla. La arkivo povas netigesar periodale.",
+       "undelete-fieldset-title": "Restaurar revizi",
+       "undeleteextrahelp": "Por restaurar omna historio de la pagino, desselektez omna buxi e kliktez <strong><em>{{int:undeletebtn}}</em></strong>.\nPor selektar quala modifiki restauresos, markizez la buxi korespondanta a la revizi por restaurar, e kliktez <strong><em>{{int:undeletebtn}}</em></strong>.",
        "undeleterevisions": "$1 {{PLURAL:$1|revizo|revizi}} efacita",
        "undeletehistory": "Se vu restauros la pagino, omna antea revizi restauresos en la korespondanta historiala pagino.\nSe nova pagino kun la sama titulo kreesis pos l'efaco, la restaurita revizuri aparos en lua historiala pagino.",
+       "undeleterevdel": "La restauro ne facesos se to produktos partala o totala efaco de la maxim recenta revizo.\nCa kazi, vu mustas desselektar o trovar la maxim recenta versiono efacita.",
+       "undeletehistorynoadmin": "Ica pagino efacesis.\nLa motivo por l'efaco montresas en la rezumo adinfre, kune la detaligo dil uzeri qui redaktis la pagino ante lua efaco.\nLa kompleta texto di ca revizi efacita esas videbla nur por l'administreri.",
        "undeleterevision-missing": "Nevalida o mankanta revizo.\nSive vu skribis la ligilo nekorekte, sive la revizo restauresis o removesis del arkivo.",
        "undeletebtn": "Restaurar",
        "undeletelink": "vidar/restaurar",
        "undeleteviewlink": "videz",
+       "undeleteinvert": "Inversigar selektajo",
        "undeletecomment": "Motivo:",
        "undeletedpage": "<strong>$1 restauresis</strong>\n\nVidez la [[Special:Log/delete|'log' pri efaci]] por vidar omna recenta efaci e restauri.",
        "undelete-search-box": "Serchez efacita pagini",
        "tags-hitcount": "$1 {{PLURAL:$1|chanjo|chanji}}",
        "tags-create-explanation": "Segun predefino, la nova etiketi kreita divenos disponebla por uzado, sive da uzeri, sive da informatikoprogrami 'bot'.",
        "tags-create-warnings-above": "La sequanta {{PLURAL:$2|avizo|avizi}} renkontresis, probante kreir l'etiketo \"$1\":",
+       "tags-delete-not-found": "L'etiketo \"$1\" ne existas.",
        "tags-delete-too-many-uses": "L'etiketo \"$1\" uzesas en plua kam $2 {{PLURAL:$2|revizo|revizi}}, do ol ne povas eskartesar.",
        "tags-delete-warnings-after-delete": "L'etiketo \"$1\" efacesis, ma la sequanta {{PLURAL:$2|avizo|avizi}} renkontresis:",
        "tags-activate-not-found": "L'etiketo \"$1\" ne existas.",
        "logentry-protect-modify-cascade": "$1 {{GENDER:$2|modifikis}} la nivelo di protekto di $3 $4 [kaskade]",
        "logentry-upload-upload": "$1 {{GENDER:$2|uploaded}} $3",
        "logentry-upload-overwrite": "$1 {{GENDER:$2|parsendis}} nova versiono di $3",
+       "log-name-tag": "Protokolo di etiketi",
        "rightsnone": "(nula)",
        "searchsuggest-search": "Serchez en {{SITENAME}}",
        "searchsuggest-containing": "quan kontenas...",
index 3e5182e..321c4eb 100644 (file)
        "rcfilters-filter-reviewstatus-unpatrolled-description": "手動または自動で巡回されていない編集。",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "未巡回",
        "rcfilters-filter-reviewstatus-manual-description": "巡回済みと手動でマークされた編集。",
+       "rcfilters-filter-reviewstatus-manual-label": "手動巡回",
+       "rcfilters-filter-reviewstatus-auto-description": "巡回済みと自動でマークされた編集。",
+       "rcfilters-filter-reviewstatus-auto-label": "自動巡回",
        "rcfilters-filtergroup-significance": "重要度",
        "rcfilters-filter-minor-label": "細部の編集",
        "rcfilters-filter-minor-description": "編集者が細部の編集とマークしたもの。",
index 0d85f3b..f4aa034 100644 (file)
@@ -17,7 +17,8 @@
                        "아라",
                        "Macofe",
                        "Matma Rex",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Notanotheramy"
                ]
        },
        "tog-underline": "Garis ngisori pranala:",
        "savechanges": "Simpen owahan",
        "publishpage": "Babar kaca",
        "publishchanges": "Babar owahan",
+       "publishchanges-start": "Babar owahan...",
        "preview": "Pratuduh",
        "showpreview": "Deleng pratuduh",
        "showdiff": "Tuduhaké owahan",
index 8bea39a..d3bc002 100644 (file)
        "botpasswords-existing": "Aktuell Botpasswierder.",
        "botpasswords-createnew": "En neit Botpasswuert uleeën",
        "botpasswords-editexisting": "E Botpasswuert änneren",
+       "botpasswords-label-needsreset": "(Passwuert muss zréckgesat ginn)",
        "botpasswords-label-appid": "Numm vum Bot:",
        "botpasswords-label-create": "Uleeën",
        "botpasswords-label-update": "Aktualiséieren",
        "recentchangeslinked-feed": "Ännerungen op verlinkt Säiten",
        "recentchangeslinked-toolbox": "Ännerungen op verlinkt Säiten",
        "recentchangeslinked-title": "Ännerungen a Verbindung mat \"$1\"",
-       "recentchangeslinked-summary": "Gitt den Numm vun enger Säit a fir Ännerungen Säiten ze gesinn op déi oder vun deene gelinkt gëtt. Ännerungen op Säite vun [[Special:Watchlist|Ärer Iwwerwaachungslëscht]] si <strong>fett</strong> geschriwwen.",
+       "recentchangeslinked-summary": "Gitt den Numm vun enger Säit a fir Ännerungen op Säiten ze gesinn op déi oder vun deene gelinkt gëtt. (Fir d'Membere vun enger Kategorie ze gesinn gitt {{ns:category}}:Numm vun der Kategorie, an.) Ännerungen op Säite vun [[Special:Watchlist|Ärer Iwwerwaachungslëscht]] si <strong>fett</strong> geschriwwen.",
        "recentchangeslinked-page": "Säitennumm:",
        "recentchangeslinked-to": "Weis Ännerungen zu de verlinkte Säiten aplaz vun der gefroter Säit",
        "recentchanges-page-added-to-category": "[[:$1]] an d'Kategorie dobäigesat",
index bad0415..4f61fd5 100644 (file)
@@ -10,7 +10,8 @@
                        "Katxis",
                        "Chabi",
                        "Angel Blaise",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Robin van der Vliet"
                ]
        },
        "tog-underline": "Sulini de lias:",
        "disclaimerpage": "Project:Renunsia jeneral",
        "edithelp": "Aida sur edita",
        "helppage-top-gethelp": "Aida",
-       "mainpage": "Paje Xef",
+       "mainpage": "Paje xef",
        "mainpage-description": "Paje xef",
        "policy-url": "Project:Politica",
        "portal": "Porton de comunia",
        "subject-preview": "Previde de tema:",
        "previewerrortext": "Un era ia aveni en atenta previde tua cambias.",
        "blockedtitle": "Usor es impedida",
-       "blockedtext": "<strong>Tua nom de usor o adirije IP es impedida.</strong>\n\nLa impedi ia es fada par $1.\nLa razona donada es ''$2''.\n\n* Comensa de impedi: $8\n* Fini de impedi: $6\n* Conta impedida: $7\n\nTu pote contata $1 o un otra [[{{MediaWiki:Grouppage-sysop}}|dirijor]] per discute esta impedi.\nTu no pote usa la funsiona \"envia un eposta a esta usor\" estra si un adirije valida\nde eposta es spesifada en tua [[Special:Preferences|preferes de conta]] e tu no es impedida de usa lo.\nTua adirije IP presente es $3, e la numero de impedi es #$5.\nInclui tota esta detalias en cualce demandas cual tu fa, per favore.",
+       "blockedtext": "<strong>Tua nom de usor o adirije IP es impedida.</strong>\n\nLa impedi ia es fada par $1.\nLa razona donada es <em>$2</em>.\n\n* Comensa de impedi: $8\n* Fini de impedi: $6\n* Conta impedida: $7\n\nTu pote contata $1 o un otra [[{{MediaWiki:Grouppage-sysop}}|dirijor]] per discute esta impedi.\nTu no pote usa la funsiona \"{{int:emailuser}}\" estra si un adirije valida\nde eposta es spesifada en tua [[Special:Preferences|preferes de conta]] e tu no es impedida de usa lo.\nTua adirije IP presente es $3, e la numero de impedi es #$5.\nInclui tota esta detalias en cualce demandas cual tu fa, per favore.",
        "autoblockedtext": "Tua adirije IP ia es automata impedida car lo ia es usada par un otra usor, ci ia es impedida par $1.\nLa razona donada es ''$2''.\n\n* Comensa de impedi: $8\n* Fini de impedi: $6\n* Conta impedida: $7\n\nTu pote contata $1 o un otra [[{{MediaWiki:Grouppage-sysop}}|dirijor]] per discute esta impedi.\nTu no pote usa la funsiona \"envia un eposta a esta usor\" estra si un adirije valida de eposta es spesifada en tua [[Special:Preferences|preferes de conta]] e tu no es impedida de usa lo.\nTua adirije IP presente es $3, e la numero de impedi es #$5.\nInclui tota esta detalias en cualce demandas cual tu fa, per favore.",
        "systemblockedtext": "Tua nom de usor o adirije IP ia es automata impedida par MediaWiki.\nLa razona donada es <em>$2</em>.\n\n* Comensa de impedi: $8\n* Fini de impedi: $6\n* Conta impedida: $7\nTua adirije IP presente es $3.\nInclui tota esta detalias en cualce demandas cual tu fa, per favore.",
        "blockednoreason": "no razona donada",
        "recentchangeslinked-feed": "Cambias relatada",
        "recentchangeslinked-toolbox": "Cambias relatada",
        "recentchangeslinked-title": "Cambias relatada a \"$1\"",
-       "recentchangeslinked-summary": "Tape un nom de paje per vide cambias en pajes liada a o de acel paje. (Per vide membros de un categoria, tape <strong>bold</strong>.) Cambias a pajes en [[Special:Watchlist|tua lista monitorida]] es <strong>spesa</strong>.",
+       "recentchangeslinked-summary": "Tape un nom de paje per vide cambias en pajes liada a o de acel paje. (Per vide membros de un categoria, tape {{ns:category}}:Nom de categoria.) Cambias a pajes en [[Special:Watchlist|tua lista monitorida]] es <strong>spesa</strong>.",
        "recentchangeslinked-page": "Nom de paje:",
        "recentchangeslinked-to": "Mostra cambias a pajes cual lia a la paje indicada, en loca",
        "recentchanges-page-added-to-category": "[[:$1]] ajuntada a categoria",
        "apisandbox-dynamic-error-exists": "Un parametre nomida \"$1\" esiste ja.",
        "apisandbox-deprecated-parameters": "Parametres desaprobada",
        "apisandbox-fetch-token": "Autopleni la marca",
+       "apisandbox-add-multi": "Ajunta",
        "apisandbox-submit-invalid-fields-title": "Alga campos es nonvalida",
        "apisandbox-submit-invalid-fields-message": "Coreti la campos indicada, per favore, e reatenta.",
        "apisandbox-results": "Resultas",
        "limitreport-templateargumentsize-value": "$1/$2 {{PLURAL:$2|bait|baites}}",
        "limitreport-expansiondepth": "Profondia la plu grande de despaci",
        "limitreport-expensivefunctioncount": "Cuantia de funsionas custosa de analisador sintatical",
+       "limitreport-unstrip-size-value": "$1/$2 {{PLURAL:$2|bait|baites}}",
        "expandtemplates": "Despaci stensiles",
        "expand_templates_intro": "Esta paje spesial prende vicitesto e despaci tota stensiles en lo, en modo recorsante. Lo despaci ance funsionas suportada de analisador sintatical como <code><nowiki>{{</nowiki>#language:…}}</code> e variables como <code><nowiki>{{</nowiki>CURRENTDAY}}</code>. En fato, lo despaci cuasi tota cosas entre brasetas risa duple.",
        "expand_templates_title": "Titulo de contesto, per {{FULLPAGENAME}}, etc.:",
index 7783806..605bac6 100644 (file)
        "subject-preview": "Преглед на насловот:",
        "previewerrortext": "Се појави грешка при обидот да се прегледаат промените.",
        "blockedtitle": "Корисникот е блокиран",
-       "blockedtext": "'''Вашето корисничко име или IP-адреса е блокирано.'''\n\nБлокирањето е направено од страна на $1.\nДаденото образложение е ''$2''.\n\n* Почеток на блокирањето: $8\n* Истекување на блокирањето: $6\n* Корисникот што требало да биде блокиран: $7\n\nМоже да контактирате со $1 или некој друг [[{{MediaWiki:Grouppage-sysop}}|администратор]] за да разговарате во врска со блокирањето.\nМожете да ја искористите можноста „Е-пошта до овој корисник“ ако е назначена важечка е-поштенска адреса во [[Special:Preferences|вашите нагодувања]] и не ви е забрането да ја користите.\nВашата сегашна IP-адреса е $3, а назнака на блокирањето гласи #$5.\nВе молиме наведете ги сите подробности прикажани погоре, во вашата евентуална реакција.",
-       "autoblockedtext": "Вашата IP-адреса е автоматски блокирана бидејќи била користена од страна на друг корисник, кој бил блокиран од $1.\nДаденото образложение е следново:\n\n:''$2''\n\n* Почеток на блокирањето: $8\n* Истекување на блокирањето: $6\n* Со намера да се блокира: $7\n\nМоже да контактирате со $1 или некој друг [[{{MediaWiki:Grouppage-sysop}}|администратор]] за да разговарате во врска со ова блокирање.\n\nИмајте предвид дека можеби нема да можете да ја искористите можноста „Е-пошта до овој корисник“ доколку не е назначена важечка е-поштенска адреса во [[Special:Preferences|вашите нагодувања]] и ви е забрането користитење на истата.\n\nВашата IP-адреса е $3, a ID на блокирањеto е $5.\nВе молиме наведете ги овие подробности доколку реагирате на блокирањето.",
+       "blockedtext": "<strong>Вашето корисничко име или IP-адреса е блокирано.</strong>\n\nБлокирањето е направено од страна на $1.\nДаденото образложение е ''$2''.\n\n* Почеток на блокирањето: $8\n* Истекување на блокирањето: $6\n* Корисникот што требало да биде блокиран: $7\n\nМоже да контактирате со $1 или некој друг [[{{MediaWiki:Grouppage-sysop}}|администратор]] за да разговарате во врска со блокирањето.\nМожете да ја искористите можноста „{{int:emailuser}}“ ако е назначена важечка е-поштенска адреса во [[Special:Preferences|вашите нагодувања]] и не ви е забрането да ја користите.\nВашата сегашна IP-адреса е $3, а назнака на блокирањето гласи #$5.\nВе молиме наведете ги сите подробности прикажани погоре, во вашата евентуална реакција.",
+       "autoblockedtext": "Вашата IP-адреса е автоматски блокирана бидејќи била користена од страна на друг корисник, кој бил блокиран од $1.\nДаденото образложение е следново:\n\n:<em>$2</em>\n\n* Почеток на блокирањето: $8\n* Истекување на блокирањето: $6\n* Со намера да се блокира: $7\n\nМоже да контактирате со $1 или некој друг [[{{MediaWiki:Grouppage-sysop}}|администратор]] за да разговарате во врска со ова блокирање.\n\nИмајте предвид дека можеби нема да можете да ја искористите можноста „{{int:emailuser}}“ доколку не е назначена важечка е-поштенска адреса во [[Special:Preferences|вашите нагодувања]] и ви е забрането користитење на истата.\n\nВашата IP-адреса е $3, a ID на блокирањеto е $5.\nВе молиме наведете ги овие подробности доколку реагирате на блокирањето.",
        "systemblockedtext": "Вашето корисничко име или IP-адреса е автоматски блокирано од МедијаВики.\nПонудена причина:\n\n:<em>$2</em>\n\n* Почеток на блокот: $8\n* Истек на блокот: $6\n* Блокот е наменет за: $7\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.",
        "blockednoreason": "не е наведена причина",
        "whitelistedittext": "Мора да сте $1 за да уредувате страници.",
        "special-characters-group-hebrew": "Хебрејски",
        "special-characters-group-bangla": "Бенгалски",
        "special-characters-group-tamil": "тамилски",
-       "special-characters-group-telugu": "Телугу",
+       "special-characters-group-telugu": "Телушки",
        "special-characters-group-sinhala": "Синхалски",
-       "special-characters-group-gujarati": "Гуџарати",
+       "special-characters-group-gujarati": "Гуџаратски",
        "special-characters-group-devanagari": "деванагари",
        "special-characters-group-thai": "Тајландски",
        "special-characters-group-lao": "Лаошки",
index 656ef4f..a412a59 100644 (file)
        "tog-enotifminoredits": "Mij e-mailen bij kleine bewerkingen van pagina’s en bestanden op mijn volglijst",
        "tog-enotifrevealaddr": "Mijn e-mailadres weergeven in e-mailberichten",
        "tog-shownumberswatching": "Het aantal gebruikers weergeven dat deze pagina volgt",
-       "tog-oldsig": "Uw bestaande ondertekening:",
+       "tog-oldsig": "Uw bestaande handtekening:",
        "tog-fancysig": "Handtekening als wikitekst behandelen (zonder automatische koppeling)",
        "tog-uselivepreview": "Voorvertoning weergeven zonder de pagina opnieuw te laden",
        "tog-forceeditsummary": "Een melding geven bij een lege bewerkingssamenvatting",
        "subject-preview": "Voorvertoning van het onderwerp:",
        "previewerrortext": "Er is een fout opgetreden tijdens het weergeven van uw wijzigingen.",
        "blockedtitle": "Gebruiker is geblokkeerd",
-       "blockedtext": "'''Uw gebruikersaccount of IP-adres is geblokkeerd.'''\n\nDe blokkade is uitgevoerd door $1.\nDe opgegeven reden is ''$2''.\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\nU kunt geen gebruik maken van de functie \"Deze gebruiker e-mailen\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]] en het gebruik van deze functie niet geblokkeerd is.\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
-       "autoblockedtext": "Uw IP-adres is automatisch geblokkeerd, omdat het gebruikt is door een andere gebruiker, die geblokkeerd is door $1.\nDe opgegeven reden is:\n\n:''$2''\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\n\nU kunt geen gebruik maken van de functie \"Deze gebruiker e-mailen\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]], en het gebruik van deze functie niet is geblokkeerd.\n\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
+       "blockedtext": "'''Uw gebruikersaccount of IP-adres is geblokkeerd.'''\n\nDe blokkade is uitgevoerd door $1.\nDe opgegeven reden is ''$2''.\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\nU kunt geen gebruik maken van de functie \"{{int:emailuser}}\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]] en het gebruik van deze functie niet geblokkeerd is.\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
+       "autoblockedtext": "Uw IP-adres is automatisch geblokkeerd, omdat het gebruikt is door een andere gebruiker, die geblokkeerd is door $1.\nDe opgegeven reden is:\n\n:''$2''\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nU kunt contact opnemen met $1 of een andere [[{{MediaWiki:Grouppage-sysop}}|beheerder]] om de blokkade te bespreken.\n\nU kunt geen gebruik maken van de functie \"{{ing:emailuser}}\", tenzij u een geldig e-mailadres hebt opgegeven in uw [[Special:Preferences|voorkeuren]], en het gebruik van deze functie niet is geblokkeerd.\n\nUw huidige IP-adres is $3 en het blokkadenummer is #$5.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
        "systemblockedtext": "Uw gebruikersaccount of IP-adres is automatisch geblokkeerd door MediaWiki.\nDe opgegeven reden is:\n\n:<em>$2</em>\n\n* Aanvang blokkade: $8\n* Einde blokkade: $6\n* Bedoeld te blokkeren: $7\n\nUw huidige IP-adres is $3.\nVermeld alle bovenstaande gegevens als u ergens op deze blokkade reageert.",
        "blockednoreason": "geen reden opgegeven",
        "whitelistedittext": "U moet $1 om pagina's te bewerken.",
index f11e4fd..d1dbadb 100644 (file)
        "content-json-empty-object": "Objècte void",
        "content-json-empty-array": "Tablèu void",
        "duplicate-args-category": "Paginas utilizant d'arguments duplicats dins los apèls de modèl",
+       "duplicate-args-category-desc": "La pagina conten de cridas a patrons qu'emplagan d'arguments duplicats, coma  <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "expensive-parserfunction-warning": "Atencion : Aquesta pagina conten tròp d’apèls dispendioses de foncions del parser.\n\nI deurià aver mens de {{PLURAL:$2|ampèl|ampèls}}, e actualament {{PLURAL:$1|i a $1 ampèl|i a $1 ampèls}}..",
        "expensive-parserfunction-category": "Paginas amb tròp d’apèls dispendioses de foncions parsaires",
        "post-expand-template-inclusion-warning": "Atencion : Aquesta pagina conten tròp d'inclusions de modèls.\nD'unas inclusions seràn pas efectuadas.",
        "post-expand-template-argument-warning": "Atencion : Aquesta pagina conten al mens un paramètre de modèl que l'inclusion es renduda impossibla. Aprèp extension, aqueste auriá produit un resultat tròp long, doncas, es pas estat inclús.",
        "post-expand-template-argument-category": "Paginas que contenon al mens un paramètre de modèl pas evaluat",
        "parser-template-loop-warning": "Modèl en bocla detectat : [[$1]]",
+       "template-loop-category": "Paginas amb boclas de patron",
+       "template-loop-category-desc": "La pagina conten una bocla dins lo patron, es a dire, un patron que se sona el meteis recursivament.",
+       "template-loop-warning": "<strong>Attencion:</strong> Aquesta pagina sona [[:$1]. Aquò es l'encausa d'una bocla de patron (una sonada infinida resursiva).",
        "parser-template-recursion-depth-warning": "Limit de longor de la recursion del modèl depassat ($1)",
        "language-converter-depth-warning": "Limit de prigondor del convertissor de lenga depassada ($1)",
        "node-count-exceeded-category": "Paginas ont nombre de nosèls es depassat",
        "prefs-watchlist-edits": "Nombre maximal de modificacions d'afichar dins la lista de seguiment :",
        "prefs-watchlist-edits-max": "Nombre maximum : 1000",
        "prefs-watchlist-token": "Geton per la lista de seguiment :",
+       "prefs-watchlist-managetokens": "Administrar los getons",
        "prefs-misc": "Preferéncias divèrsas",
        "prefs-resetpass": "Modificar lo senhal",
        "prefs-changeemail": "Cambiar o suprimir l'adreça electronica",
        "recentchangescount": "Nombre de modificacions d'afichar per defauta dins los cambiaments recents, los istorics e los logs :",
        "prefs-help-recentchangescount": "Nombre maximum : 1000",
        "prefs-help-watchlist-token2": "Aquí la clau secreta del flux Web de vòstra lista de seguiment.\nTota persona que la coneis poirà legir vòstra lista de seguiment, doncas, la comuniquetz pas.\nSe necessari, [[Special:ResetTokens|clicatz aicí per la reïnicializar]].",
+       "prefs-help-tokenmanagement": "Podètz veire e tornar inicializar la clau secreta del vòstre compte que pòt accedir al flux Web de la vòstre lista de seguit. Tota persona que coneis la clau poirà legir la vòstra lista, alara la compartissètz pas",
        "savedprefs": "Las preferéncias son estadas salvadas.",
        "savedrights": "Los dreits d'utilizaire de {{GENDER:$1|$1}} son estats enregistrats.",
        "timezonelegend": "Fus orari :",
index 877184c..7818961 100644 (file)
        "botpasswords-existing": "Senhas de robôs existentes",
        "botpasswords-createnew": "Crie uma nova senha de robô",
        "botpasswords-editexisting": "Editar uma senha de robô existente",
+       "botpasswords-label-needsreset": "(senha precisa ser redefinida)",
        "botpasswords-label-appid": "Nome do robô:",
        "botpasswords-label-create": "Criar",
        "botpasswords-label-update": "Atualizar",
        "botpasswords-restriction-failed": "Restrições de senha de robô evitam esta autenticação.",
        "botpasswords-invalid-name": "O nome de usuário especificado não contém o separador de senha de robô (\"$1\").",
        "botpasswords-not-exist": "O usuário \"$1\" não possui uma senha de robô \"$2\".",
+       "botpasswords-needs-reset": "A senha do robô de nome \"$2\" {{GENDER:$1|do usuário|da usuária}} \"$1\" deve ser redefinida.",
        "resetpass_forbidden": "As senhas não podem ser alteradas",
        "resetpass_forbidden-reason": "Senhas não podem ser alteradas: $1",
        "resetpass-no-info": "Você precisa estar autenticado para acessar esta página diretamente.",
        "recentchangescount": "Número de edições a apresentar por omissão nas mudanças recentes, nos historiais de páginas e nos registos:",
        "prefs-help-recentchangescount": "Número máximo: 1000",
        "prefs-help-watchlist-token2": "Esta é a senha secreta para o feed da Web com sua lista de tokens vigiados.\nQualquer pessoa que descobrir esta senha será capaz de ler sua lista, então não a compartilhe.\nSe você precisar [[Special:ResetTokens|você pode redefini-lo]].",
-       "prefs-help-tokenmanagement": "Você pode ver e redefinir a chave secreta para sua conta que pode acessar o feed da Web da sua lista de vigilância. Qualquer pessoa que conheça a chave poderá ler sua lista de observação, então não compartilhe.",
+       "prefs-help-tokenmanagement": "Pode ver e repor a chave secreta da sua conta que permite aceder ao feed da sua lista de páginas vigiadas. Qualquer pessoa que conheça a chave será capaz de ler a sua lista de páginas vigiadas, por isso não a partilhe.",
        "savedprefs": "As suas preferências foram salvas.",
        "savedrights": "Os grupos {{GENDER:$1|do usuário|da usuária}} $1 foram gravados.",
        "timezonelegend": "Fuso horário:",
        "prefs-custom-json": "JSON personalizado",
        "prefs-custom-js": "JS personalizado",
        "prefs-common-config": "CSS/JSON/JavaScript compartilhado por todos os temas:",
-       "prefs-reset-intro": "Você pode usar esta página para restaurar as suas preferências para os valores predefinidos do sítio.\nEsta ação não pode ser desfeita.",
+       "prefs-reset-intro": "Pode usar esta página para repor as configurações padrão das preferências.\nAs suas preferências serão modificadas para os valores predefinidos do site.\nEsta operação não pode ser desfeita.",
        "prefs-emailconfirm-label": "Confirmação do e-mail:",
        "youremail": "Seu e-mail:",
        "username": "Nome de {{GENDER:$1|usuário|usuária|usuário(a)}}:",
        "recentchangeslinked-feed": "Mudanças relacionadas",
        "recentchangeslinked-toolbox": "Mudanças relacionadas",
        "recentchangeslinked-title": "Mudanças relacionadas com “$1”",
-       "recentchangeslinked-summary": "Digite um nome de página para ver as alterações nas páginas vinculadas ou a partir dessa página. (Para ver membros de uma categoria, digite Categoria: Nome da categoria). Mudanças nas páginas em [[Special:Watchlist|lista de páginas vigiadas]] são exibidas em <strong>negrito<strong>",
+       "recentchangeslinked-summary": "Introduza o nome de uma página para ver as mudanças a todas as páginas que contêm hiperligações para ela ou para as quais a página fornecida contém hiperligações (para ver as que pertencem a uma categoria, introduza {{ns:category}}:Nome da categoria). As mudanças às suas [[Special:Watchlist|páginas vigiadas]] aparecem a <strong>negrito</strong>.",
        "recentchangeslinked-page": "Nome da página:",
        "recentchangeslinked-to": "Inversamente, mostrar mudanças nas páginas que contêm ligações para esta",
        "recentchanges-page-added-to-category": "[[:$1]]adicionada à categoria",
index 9a67d6f..40a8065 100644 (file)
@@ -76,7 +76,8 @@
                        "Ngl2016",
                        "RadiX",
                        "MokaAkashiyaPT",
-                       "Athena in Wonderland"
+                       "Athena in Wonderland",
+                       "Fitoschido"
                ]
        },
        "tog-underline": "Sublinhar hiperligações:",
        "botpasswords-existing": "Palavras-passe de robô existentes",
        "botpasswords-createnew": "Criar uma nova palavra-passe para robô",
        "botpasswords-editexisting": "Editar uma palavra-passe de robô existente",
+       "botpasswords-label-needsreset": "(a palavra-passe precisa ser redefinida)",
        "botpasswords-label-appid": "Nome do robô:",
        "botpasswords-label-create": "Criar",
        "botpasswords-label-update": "Atualizar",
        "botpasswords-restriction-failed": "Restrições da palavra-passe de robô impedem esta autenticação.",
        "botpasswords-invalid-name": "O nome de utilizador especificado não contém o separador de palavra-passe de robô (\"$1\").",
        "botpasswords-not-exist": "O utilizador \"$1\" não tem uma palavra-passe para o robô chamado \"$2\".",
+       "botpasswords-needs-reset": "A palavra-passe do robô de nome \"$2\" {{GENDER:$1|do utilizador|da utilizadora}} \"$1\" deve ser redefinida.",
        "resetpass_forbidden": "As palavras-passe não podem ser alteradas",
        "resetpass_forbidden-reason": "As palavras-passe não podem ser alteradas: $1",
        "resetpass-no-info": "Precisa de iniciar sessão para aceder diretamente a esta página.",
        "subject-preview": "Antevisão do assunto:",
        "previewerrortext": "Ocorreu um erro enquanto tentava antever as suas alterações.",
        "blockedtitle": "O utilizador está bloqueado",
-       "blockedtext": "<strong>O seu nome de utilizador ou endereço IP foram bloqueados.</strong>\n\nO bloqueio foi realizado por $1.\nO motivo apresentado foi <em>$2</em>.\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nPode contactar $1 ou outro [[{{MediaWiki:Grouppage-sysop}}|administrador]] para discutir o bloqueio.\nNote que para utilizar a funcionalidade \"Contactar utilizador\" precisa de ter um endereço de correio eletrónico válido nas suas [[Special:Preferences|preferências]] e de não lhe ter sido bloqueado o uso desta funcionalidade.\nO seu endereço IP neste momento é $3 e a identificação (ID) do bloqueio é #$5.\nInclua todos os detalhes acima em quaisquer contactos relacionados com este bloqueio, por favor.",
-       "autoblockedtext": "O seu endereço IP foi bloqueado de forma automática porque foi utilizado recentemente por outro utilizador, o qual foi bloqueado por $1.\nO motivo apresentado foi:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nPode contactar $1 ou outro [[{{MediaWiki:Grouppage-sysop}}|administrador]] para discutir o bloqueio.\n\nNote que para utilizar a funcionalidade \"Contactar utilizador\" precisa de ter um endereço de correio eletrónico válido nas suas [[Special:Preferences|preferências]] e de não lhe ter sido bloqueado o uso desta funcionalidade.\n\nO seu endereço IP neste momento é $3 e a identificação (ID) do bloqueio é #$5.\nInclua todos os detalhes acima em quaisquer contactos relacionados com este bloqueio, por favor.",
+       "blockedtext": "<strong>O seu nome de utilizador ou endereço IP foram bloqueados.</strong>\n\nO bloqueio foi realizado por $1.\nO motivo apresentado foi <em>$2</em>.\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nPode contactar $1 ou outro [[{{MediaWiki:Grouppage-sysop}}|administrador]] para discutir o bloqueio.\nNote que para utilizar a funcionalidade \"{{int:emailuser}}\" precisa de ter um endereço de correio eletrónico válido nas suas [[Special:Preferences|preferências]] e de não lhe ter sido bloqueado o uso desta funcionalidade.\nO seu endereço IP neste momento é $3 e a identificação (ID) do bloqueio é #$5.\nInclua todos os detalhes acima em quaisquer contactos relacionados com este bloqueio, por favor.",
+       "autoblockedtext": "O seu endereço IP foi bloqueado de forma automática porque foi utilizado recentemente por outro utilizador, o qual foi bloqueado por $1.\nO motivo apresentado foi:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nPode contactar $1 ou outro [[{{MediaWiki:Grouppage-sysop}}|administrador]] para discutir o bloqueio.\n\nNote que para utilizar a funcionalidade \"{{int:emailuser}}\" precisa de ter um endereço de correio eletrónico válido nas suas [[Special:Preferences|preferências]] e de não lhe ter sido bloqueado o uso desta funcionalidade.\n\nO seu endereço IP neste momento é $3 e a identificação (ID) do bloqueio é #$5.\nInclua todos os detalhes acima em quaisquer contactos relacionados com este bloqueio, por favor.",
        "systemblockedtext": "O seu nome de utilizador ou endereço IP foram bloqueados automaticamente pelo MediaWiki.\nO motivo fornecido é:\n\n:<em>$2</em>\n\n* Início do bloqueio: $8\n* Expiração do bloqueio: $6\n* Destinatário do bloqueio: $7\n\nO seu endereço IP atual é $3.\nInclua todos os detalhes acima em quaisquer contactos sobre este assunto, por favor.",
        "blockednoreason": "sem motivo especificado",
        "whitelistedittext": "Precisa de $1 para poder editar páginas.",
        "recentchangeslinked-feed": "Alterações relacionadas",
        "recentchangeslinked-toolbox": "Alterações relacionadas",
        "recentchangeslinked-title": "Alterações relacionadas com \"$1\"",
-       "recentchangeslinked-summary": "Introduza o nome de uma página para ver as mudanças a todas as páginas que contêm hiperligações para ela ou para as quais a página fornecida contém hiperligações (para ver as que pertencem a uma categoria, introduza Categoria:Nome da categoria). As mudanças às suas [[Special:Watchlist|páginas vigiadas]] aparecem a <strong>negrito</strong>.",
+       "recentchangeslinked-summary": "Introduza o nome de uma página para ver as mudanças a todas as páginas que contêm hiperligações para ela ou para as quais a página fornecida contém hiperligações (para ver as que pertencem a uma categoria, introduza {{ns:category}}:Nome da categoria). As mudanças às suas [[Special:Watchlist|páginas vigiadas]] aparecem a <strong>negrito</strong>.",
        "recentchangeslinked-page": "Nome da página:",
        "recentchangeslinked-to": "Inversamente, mostrar mudanças às páginas que contêm hiperligações para esta",
        "recentchanges-page-added-to-category": "[[:$1]] foi adicionada à categoria",
index 99d0548..e4efea8 100644 (file)
        "botpasswords-existing": "Существующие пароли бота",
        "botpasswords-createnew": "Создать новый пароль бота",
        "botpasswords-editexisting": "Редактировать существующий пароль бота",
+       "botpasswords-label-needsreset": "(пароль должен быть сброшен)",
        "botpasswords-label-appid": "Название бота:",
        "botpasswords-label-create": "Создать",
        "botpasswords-label-update": "Обновить",
        "botpasswords-restriction-failed": "Из-за ограничений, связанных с паролем бота, вход не произведён.",
        "botpasswords-invalid-name": "Указанное имя участника не содержит разделителя для пароля бота («$1»).",
        "botpasswords-not-exist": "У участника «$1» нет пароля для бота с названием «$2».",
+       "botpasswords-needs-reset": "Пароль для бота «$1» {{GENDER:$2|участника|участницы}} «$2» должен быть сброшен.",
        "resetpass_forbidden": "Пароль не может быть изменён",
        "resetpass_forbidden-reason": "Пароли не могут быть изменены: $1",
        "resetpass-no-info": "Чтобы обращаться непосредственно к этой странице, вам следует представиться системе.",
index 0ee213e..d549a85 100644 (file)
        "yourdiff": "فرق",
        "templatesused": "ایں ورقے تے  ورتے ڳئے {{PLURAL:$1|سانچے|سانچہ}}:",
        "templatesusedpreview": "ایں کچے کم تے  ورتے ڳئے {{PLURAL:$1|سانچے|سانچہ}}:",
-       "template-protected": "(بÚ\86اÛ\8cا Ú¯یا)",
+       "template-protected": "(بÚ\86اÛ\8cا Ú³یا)",
        "template-semiprotected": "(نیم محفوظ)",
        "hiddencategories": "ایہ ورقہ {{PLURAL:$1|1 لُکے زمریاں|$1 لکا زمرہ }} وچ شامل ہے:",
        "permissionserrors": "خطائے اجازت",
        "logentry-contentmodel-change-revertlink": "واپس",
        "logentry-contentmodel-change-revert": "واپس",
        "protectlogpage": "حفاظت لاگ",
-       "protectedarticle": "\"[[$1]]\" Ø¨Ú\86اÛ\8cا Ú¯Û\8cا Ø§ے",
+       "protectedarticle": "\"[[$1]]\" Ø¨Ú\86اÛ\8cا Ú³Û\8cا Û\81ے",
        "modifiedarticleprotection": "«[[$1]]» دا درجہ حفاظت تبدیل کیتا",
        "protectcomment": "سبب:",
        "protectexpiry": "مُکسی:",
index f3e9296..f314ca4 100644 (file)
        "botpasswords-existing": "Obstoječa gesla botov",
        "botpasswords-createnew": "Ustvari novo geslo bota",
        "botpasswords-editexisting": "Uredi obstoječe geslo bota",
+       "botpasswords-label-needsreset": "(geslo mora biti ponastavljeno)",
        "botpasswords-label-appid": "Ime bota:",
        "botpasswords-label-create": "Ustvari",
        "botpasswords-label-update": "Posodobi",
        "botpasswords-restriction-failed": "Omejitve gesla bota preprečujejo to prijavo.",
        "botpasswords-invalid-name": "Navedeno uporabniško ime ne vsebuje ločila za geslo bota (»$1«).",
        "botpasswords-not-exist": "Uporabnik »$1« nima gesla bota z imenom »$2«.",
+       "botpasswords-needs-reset": "Geslo bota »$2« {{GENDER:$1|uporabnika|uporabnice}} »$1« mora biti ponastavljeno.",
        "resetpass_forbidden": "Gesla ne morete spremeniti",
        "resetpass_forbidden-reason": "Gesel nismo mogli spremeniti: $1",
        "resetpass-no-info": "Za neposreden dostop do te strani morate biti prijavljeni.",
        "subject-preview": "Predogled zadeve:",
        "previewerrortext": "Med poskusom prikaza predogleda vaših sprememb je prišlo do napake.",
        "blockedtitle": "Uporabnik je blokiran",
-       "blockedtext": "'''Urejanje z vašim uporabniškim imenom oziroma IP-naslovom je onemogočeno.'''\n\nBlokiral vas je $1.\nPodani razlog je ''$2''.\n\n* začetek blokade: $8\n* potek blokade: $6\n* blokirani uporabnik: $7\n\nO blokiranju se lahko pogovorite z uporabnikom/-co $1 ali katerim drugim [[{{MediaWiki:Grouppage-sysop}}|administratorjem]].\nVedite, da lahko ukaz »Pošlji uporabniku e-pismo« uporabite le, če ste v [[Special:Preferences|nastavitvah]] vpisali in potrdili svoj elektronski naslov in ta ni blokiran.\nVaš IP-naslov je $3, številka blokade pa #$5.\nProsimo, vključite ju v vse morebitne poizvedbe.",
-       "autoblockedtext": "Vaš IP-naslov je bil samodejno blokiran, saj je bil uporabljen s strani drugega uporabnika, ki ga je blokiral $1.\nRazlog za to je bil naslednji:\n\n:''$2''\n\n* Začetek blokade: $8\n* Konec blokade: $6\n* Blokirani uporabnik: $7\n\nKontaktirate lahko $1 ali katerega od drugih [[{{MediaWiki:Grouppage-sysop}}|administratorjev]], da razpravljate o blokadi.\n\nVedite, da lahko funkcijo »{{:MediaWiki:Emailuser/sl}}« uporabljate le, če ste v svoje [[Special:Preferences|uporabniške nastavitve]] vnesli veljaven e-poštni naslov, in vam njena uporaba ni bila preprečena.\n\nVaš trenutni IP-naslov je $3, ID blokiranja pa #$5. Prosimo, vključite ta ID v vsako zastavljeno vprašanje.",
+       "blockedtext": "<strong>Urejanje z vašim uporabniškim imenom oziroma IP-naslovom je onemogočeno.</strong>\n\nBlokiral vas je $1.\nPodani razlog je <em>$2</em>.\n\n* Začetek blokade: $8\n* Potek blokade: $6\n* Blokirani uporabnik: $7\n\nO blokiranju se lahko pogovorite z uporabnikom/-co $1 ali katerim drugim [[{{MediaWiki:Grouppage-sysop}}|administratorjem]].\nVedite, da lahko ukaz »{{int:emailuser}}« uporabite le, če ste v [[Special:Preferences|nastavitvah]] vpisali in potrdili svoj elektronski naslov in ta ni blokiran.\nVaš IP-naslov je $3, številka blokade pa #$5.\nProsimo, vključite ju v vse morebitne poizvedbe.",
+       "autoblockedtext": "Vaš IP-naslov je bil samodejno blokiran, saj je bil uporabljen s strani drugega uporabnika, ki ga je blokiral $1.\nRazlog za to je bil naslednji:\n\n:<em>$2</em>\n\n* Začetek blokade: $8\n* Konec blokade: $6\n* Blokirani uporabnik: $7\n\nKontaktirate lahko $1 ali katerega od drugih [[{{MediaWiki:Grouppage-sysop}}|administratorjev]], da razpravljate o blokadi.\n\nVedite, da lahko funkcijo »{{int:emailuser}}« uporabljate le, če ste v svoje [[Special:Preferences|uporabniške nastavitve]] vnesli veljaven e-poštni naslov, in vam njena uporaba ni bila preprečena.\n\nVaš trenutni IP-naslov je $3, ID blokiranja pa #$5. Prosimo, vključite ta ID v vsako zastavljeno vprašanje.",
        "systemblockedtext": "Vaše uporabniško ime ali IP-naslov je MediaWiki samodejn blokiral.\nPodani razlog je:\n\n:<em>$2</em>\n\n* Začetek blokade: $8\n* Potek blokade: $6\n* Blokirani uporabnik: $7\n\nVaš trenutni IP-naslov je $3.\nProsimo, da v svoje poizvedbe vključite vse zgornje podatke.",
        "blockednoreason": "razlog ni podan",
        "whitelistedittext": "Za urejanje strani se morate $1.",
index 534190a..fa0ea82 100644 (file)
        "botpasswords-existing": "Befintliga botlösenord",
        "botpasswords-createnew": "Skapa ett nytt botlösenord",
        "botpasswords-editexisting": "Redigera ett befintligt botlösenord",
+       "botpasswords-label-needsreset": "(lösenordet behöver återställas)",
        "botpasswords-label-appid": "Botnamn:",
        "botpasswords-label-create": "Skapa",
        "botpasswords-label-update": "Uppdatera",
        "botpasswords-restriction-failed": "Begränsningar av botlösenord tillåter inte denna inloggning.",
        "botpasswords-invalid-name": "Det angivna användarnamnet innehåller inte separatorn för botlösenord (\"$1\").",
        "botpasswords-not-exist": "Användaren \"$1\" har inte ett botlösenord som är \"$2\".",
+       "botpasswords-needs-reset": "Botlösenordet för botnamnet \"$2\" till {{GENDER:$1|användaren}} \"$1\" måste återställas.",
        "resetpass_forbidden": "Lösenord kan inte ändras",
        "resetpass_forbidden-reason": "Lösenorden kan inte ändras: $1",
        "resetpass-no-info": "Du måste vara inloggad för att komma åt den här sidan direkt.",
        "recentchangeslinked-feed": "Relaterade ändringar",
        "recentchangeslinked-toolbox": "Relaterade ändringar",
        "recentchangeslinked-title": "Ändringar relaterade till \"$1\"",
-       "recentchangeslinked-summary": "Ange namnet på en sida för att se ändringar på sidor som länkas till eller från denna sida. (För att se medlemmar i en kategori, skriv Kategori:Namnet på kategorin). Ändringar på sidor i [[Special:Watchlist|din bevakningslista]] är <strong>fetstilta</strong>.",
+       "recentchangeslinked-summary": "Ange namnet på en sida för att se ändringar på sidor som länkas till eller från denna sida. (För att se medlemmar i en kategori, skriv {{ns:category}}:Namnet på kategorin). Ändringar på sidor i [[Special:Watchlist|din bevakningslista]] är <strong>fetstilta</strong>.",
        "recentchangeslinked-page": "Sidnamn:",
        "recentchangeslinked-to": "Visa ändringar på sidor med länkar till den givna sidan istället",
        "recentchanges-page-added-to-category": "[[:$1]] lades till i kategorin",
index d78fcd1..2a3b845 100644 (file)
        "welcomecreation-msg": "Uotwarli my sam lo Ćebje kůnto.\nPamjyntej coby posztalować [[Special:Preferences|preferencyji]]",
        "yourname": "Mjano użytkowńika:",
        "userlogin-yourname": "Mjano używocza",
-       "userlogin-yourname-ph": "Wszkryflej swoje mjano użytkowńika",
+       "userlogin-yourname-ph": "Wkludź swoje miano używacza",
        "createacct-another-username-ph": "Wszkryflej mjano użytkowńika",
        "yourpassword": "Hasło:",
        "userlogin-yourpassword": "Hasło",
-       "userlogin-yourpassword-ph": "Wszkryflej swoje hasło",
-       "createacct-yourpassword-ph": "Wszkryflej hasło",
+       "userlogin-yourpassword-ph": "Wkludź swoje hasło",
+       "createacct-yourpassword-ph": "Wkludź hasło",
        "yourpasswordagain": "Naszkryflej ausdruk zaś",
        "createacct-yourpasswordagain": "Potwjyrdź hasło",
-       "createacct-yourpasswordagain-ph": "Wszkryflej hasło jeszcze roz",
+       "createacct-yourpasswordagain-ph": "Wkludź hasło jeszcze rŏz",
        "userlogin-remembermypassword": "Ńy wylogůwywuj mje",
        "userlogin-signwithsecure": "Użyj bezpjecznygo połůnczyńa",
        "yourdomainname": "Twoja domyna",
        "userlogin-createanother": "Twůrz inksze kůnto",
        "createacct-emailrequired": "E-brif",
        "createacct-emailoptional": "E-brif (uopcjůnalne)",
-       "createacct-email-ph": "Wszkryflej swůj adres do e-brifa",
+       "createacct-email-ph": "Wkludź swojã adresã e-brifa",
        "createacct-another-email-ph": "Nastow e-brif",
        "createaccountmail": "Użyj chwilowygo hasła losowo genyrowanygo a wyślij je na wrychtowany adres e-brifa.",
        "createacct-realname": "Prawdźiwe imje a nazwisko (uopcjůnalńe)",
index 0226e5c..db9fe96 100644 (file)
        "botpasswords-existing": "Mevcut bot parolaları",
        "botpasswords-createnew": "Yeni bir bot parolası oluştur",
        "botpasswords-editexisting": "Mevcut bir bot parolasını düzenle",
+       "botpasswords-label-needsreset": "(parolanın sıfırlanması gerekiyor)",
        "botpasswords-label-appid": "Bot ismi:",
        "botpasswords-label-create": "Oluştur",
        "botpasswords-label-update": "Güncelle",
        "botpasswords-no-provider": "BotPasswordsSessionProvider kullanılamaz.",
        "botpasswords-restriction-failed": "Bot parolası kısıtlamaları bu oturum açma işlemini önlemektedir.",
        "botpasswords-invalid-name": "Belirtilen kullanıcı adı bot parolası ayırıcısı içermiyor (\"$1\").",
+       "botpasswords-needs-reset": "\"$1\" {{GENDER:$1|kullanıcısına}} ait \"$2\" adlı bot için bot parolası sıfırlanmalı.",
        "resetpass_forbidden": "Parolalar değiştirilememektedir",
        "resetpass_forbidden-reason": "Parolalar değiştirilemez: $1",
        "resetpass-no-info": "Bu sayfaya doğrudan erişmek için oturum açmanız gereklidir.",
index 1eaf278..c890f9a 100644 (file)
        "subject-preview": "主题的预览:",
        "previewerrortext": "尝试预览您的更改时发生错误。",
        "blockedtitle": "用户被封禁",
-       "blockedtext": "<strong>您的用户名或IP地址已被封禁。</strong>\n\n执行封禁的管理员是$1。封禁原因是<em>$2</em>。\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您可以联络$1或其他[[{{MediaWiki:Grouppage-sysop}}|管理员]]讨论该封禁。只有当您在[[Special:Preferences|系统设置]]确认了电子邮件地址且未被禁止使用“电邮联系”功能时,才可以使用它。您当前的IP地址是$3,该封禁ID是#$5。请在您做出的任何查询中包含所有上述详情。",
-       "autoblockedtext": "您的IP地址因曾被一位被$1封禁的用户使用而被自动封禁。封禁原因:\n\n:<em>$2</em>\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您可以联系$1或其他[[{{MediaWiki:Grouppage-sysop}}|管理员]]申诉该封禁。\n\n请注意,只有当您已在[[Special:Preferences|系统设置]]确认了电子邮件地址且未被禁止使用“电邮联系”功能时,才能发送电子邮件联系管理员。\n\n您当前的IP地址为$3,该封禁ID为#$5。请在您做出的任何查询中包含所有上述详情。",
+       "blockedtext": "<strong>您的用户名或IP地址已被封禁。</strong>\n\n执行封禁的管理员是$1。封禁原因是<em>$2</em>。\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您可以联络$1或其他[[{{MediaWiki:Grouppage-sysop}}|管理员]]讨论该封禁。只有当您在[[Special:Preferences|系统设置]]确认了电子邮件地址且未被禁止使用“{{int:emailuser}}”功能时,才可以使用它。您当前的IP地址是$3,该封禁ID是#$5。请在您做出的任何查询中包含所有上述详情。",
+       "autoblockedtext": "您的IP地址因曾被一位被$1封禁的用户使用而被自动封禁。封禁原因:\n\n:<em>$2</em>\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您可以联系$1或其他[[{{MediaWiki:Grouppage-sysop}}|管理员]]申诉该封禁。\n\n请注意,只有当您已在[[Special:Preferences|系统设置]]确认了电子邮件地址且未被禁止使用“{{int:emailuser}}”功能时,才能发送电子邮件联系管理员。\n\n您当前的IP地址为$3,该封禁ID为#$5。请在您做出的任何查询中包含所有上述详情。",
        "systemblockedtext": "您的用户名或IP地址已被MediaWiki自动封禁。封禁原因:\n\n:<em>$2</em>\n\n* 开始时间:$8\n* 到期时间:$6\n* 目标用户:$7\n\n您当前的IP地址是$3。请在您做出的任何查询中包含所有上述详情。",
        "blockednoreason": "未给出原因",
        "whitelistedittext": "请$1以编辑页面。",
index 3a14a15..deeee82 100644 (file)
        "botpasswords-existing": "已存在機器人密碼",
        "botpasswords-createnew": "建立新機器人密碼",
        "botpasswords-editexisting": "編輯已存在的機器人密碼",
+       "botpasswords-label-needsreset": "(密碼需要重新設定)",
        "botpasswords-label-appid": "機器人名稱:",
        "botpasswords-label-create": "建立",
        "botpasswords-label-update": "更新",
        "prefs-watchlist-edits": "監視清單中顯示的變更數量上限:",
        "prefs-watchlist-edits-max": "數量上限:1000",
        "prefs-watchlist-token": "監視清單金鑰:",
+       "prefs-watchlist-managetokens": "管理令牌",
        "prefs-misc": "其他",
        "prefs-resetpass": "變更密碼",
        "prefs-changeemail": "變更或移除電子郵件地址",
diff --git a/maintenance/categoryChangesAsRdf.php b/maintenance/categoryChangesAsRdf.php
new file mode 100644 (file)
index 0000000..a12cda7
--- /dev/null
@@ -0,0 +1,542 @@
+<?php
+/**
+ * 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
+ *
+ */
+use Wikimedia\Purtle\RdfWriter;
+use Wikimedia\Purtle\TurtleRdfWriter;
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to provide RDF representation of the recent changes in category tree.
+ *
+ * @ingroup Maintenance
+ * @since 1.30
+ */
+class CategoryChangesAsRdf extends Maintenance {
+       /**
+        * Insert query
+        */
+       const SPARQL_INSERT = <<<SPARQL
+INSERT DATA {
+%s
+};
+
+SPARQL;
+
+       /**
+        * Delete/Insert query
+        */
+       const SPARQL_DELETE_INSERT = <<<SPARQLDI
+DELETE {
+?category ?x ?y
+} INSERT {
+%s
+} WHERE {
+   VALUES ?category {
+     %s
+   }
+};
+
+SPARQLDI;
+
+       /**
+        * @var RdfWriter
+        */
+       private $rdfWriter;
+       /**
+        * Categories RDF helper.
+        * @var CategoriesRdf
+        */
+       private $categoriesRdf;
+
+       private $startTS;
+       private $endTS;
+
+       /**
+        * List of processed page IDs,
+        * so we don't try to process same thing twice
+        * @var int[]
+        */
+       protected $processed = [];
+
+       public function __construct() {
+               parent::__construct();
+
+               $this->addDescription( "Generate RDF dump of category changes in a wiki." );
+
+               $this->setBatchSize( 200 );
+               $this->addOption( 'output', "Output file (default is stdout). Will be overwritten.", false,
+                       true, 'o' );
+               $this->addOption( 'start', 'Starting timestamp (inclusive), in ISO or Mediawiki format.',
+                       true, true, 's' );
+               $this->addOption( 'end', 'Ending timestamp (exclusive), in ISO or Mediawiki format.', true,
+                       true, 'e' );
+       }
+
+       /**
+        * Initialize external service classes.
+        */
+       public function initialize() {
+               // SPARQL Update syntax is close to Turtle format, so we can use Turtle writer.
+               $this->rdfWriter = new TurtleRdfWriter();
+               $this->categoriesRdf = new CategoriesRdf( $this->rdfWriter );
+       }
+
+       public function execute() {
+               global $wgRCMaxAge;
+
+               $this->initialize();
+
+               $startTS = new MWTimestamp( $this->getOption( "start" ) );
+               $endTS = new MWTimestamp( $this->getOption( "end" ) );
+               $now = new MWTimestamp();
+
+               if ( $now->getTimestamp() - $startTS->getTimestamp() > $wgRCMaxAge ) {
+                       $this->error( "Start timestamp too old, maximum RC age is $wgRCMaxAge!" );
+               }
+               if ( $now->getTimestamp() - $endTS->getTimestamp() > $wgRCMaxAge ) {
+                       $this->error( "End timestamp too old, maximum RC age is $wgRCMaxAge!" );
+               }
+
+               $this->startTS = $startTS->getTimestamp();
+               $this->endTS = $endTS->getTimestamp();
+
+               $outFile = $this->getOption( 'output', 'php://stdout' );
+               if ( $outFile === '-' ) {
+                       $outFile = 'php://stdout';
+               }
+
+               $output = fopen( $outFile, 'wb' );
+
+               $this->categoriesRdf->setupPrefixes();
+               $this->rdfWriter->start();
+
+               $prefixes = $this->getRdf();
+               // We have to strip @ from prefix, since SPARQL UPDATE doesn't use them
+               // Also strip dot at the end.
+               $prefixes = preg_replace( [ '/^@/m', '/\s*[.]$/m' ], '', $prefixes );
+               fwrite( $output, $prefixes );
+
+               $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
+
+               // Deletes go first because if the page was deleted, other changes
+               // do not matter. This only gets true deletes, i.e. not pages that were restored.
+               $this->handleDeletes( $dbr, $output );
+               // Moves go before additions because if category is moved, we should not process creation
+               // as it would produce wrong data - because create row has old title
+               $this->handleMoves( $dbr, $output );
+               // We need to handle restores too since delete may have happened in previous update.
+               $this->handleRestores( $dbr, $output );
+               $this->handleAdds( $dbr, $output );
+               $this->handleChanges( $dbr, $output );
+
+               // Update timestamp
+               fwrite( $output, $this->updateTS( $this->endTS ) );
+       }
+
+       /**
+        * Get SPARQL for updating set of categories
+        * @param IDatabase $dbr
+        * @param string[] $deleteUrls List of URIs to be deleted, with <>
+        * @param string[] $pages List of categories: id => title
+        * @param string $mark Marks which operation requests the query
+        * @return string SPARQL query
+        */
+       private function getCategoriesUpdate( IDatabase $dbr, $deleteUrls, $pages, $mark ) {
+               if ( empty( $deleteUrls ) ) {
+                       return "";
+               }
+
+               if ( !empty( $pages ) ) {
+                       $this->writeParentCategories( $dbr, $pages );
+               }
+
+               return "# $mark\n" . sprintf( self::SPARQL_DELETE_INSERT,
+                               $this->getRdf(),
+                               implode( ' ', $deleteUrls ) );
+       }
+
+       /**
+        * Write data for a set of categories
+        * @param IDatabase $dbr
+        * @param string[] $pages List of categories: id => title
+        */
+       private function writeParentCategories( IDatabase $dbr, $pages ) {
+               foreach ( $this->getCategoryLinksIterator( $dbr, array_keys( $pages ) ) as $row ) {
+                       $this->categoriesRdf->writeCategoryLinkData( $pages[$row->cl_from], $row->cl_to );
+               }
+       }
+
+       /**
+        * Generate SPARQL Update code for updating dump timestamp
+        * @param string|int $timestamp Timestamp for last change
+        * @return string SPARQL Update query for timestamp.
+        */
+       public function updateTS( $timestamp ) {
+               $dumpUrl = '<' . $this->categoriesRdf->getDumpURI() . '>';
+               $ts = wfTimestamp( TS_ISO_8601, $timestamp );
+               $tsQuery = <<<SPARQL
+DELETE {
+  $dumpUrl schema:dateModified ?o .
+}
+WHERE {
+  $dumpUrl schema:dateModified ?o .
+};
+INSERT DATA {
+  $dumpUrl schema:dateModified "$ts"^^xsd:dateTime .
+}
+
+SPARQL;
+               return $tsQuery;
+       }
+
+       /**
+        * Set up standard iterator for retrieving category changes.
+        * @param IDatabase $dbr
+        * @param string[] $columns List of additional fields to get
+        * @param string[] $extra_tables List of additional tables to join
+        * @return BatchRowIterator
+        */
+       private function setupChangesIterator(
+               IDatabase $dbr,
+               array $columns = [],
+               array $extra_tables = []
+       ) {
+               $tables = [ 'recentchanges', 'page_props', 'category' ];
+               if ( $extra_tables ) {
+                       $tables += $extra_tables;
+               }
+               $it = new BatchRowIterator( $dbr,
+                       $tables,
+                       [ 'rc_timestamp' ],
+                       $this->mBatchSize
+               );
+               $this->addTimestampConditions( $it, $dbr );
+               $it->addJoinConditions(
+                       [
+                               'page_props' => [
+                                       'LEFT JOIN', [ 'pp_propname' => 'hiddencat', 'pp_page = rc_cur_id' ]
+                               ],
+                               'category' => [
+                                       'LEFT JOIN', [ 'cat_title = rc_title' ]
+                               ]
+                       ]
+               );
+               $it->setFetchColumns( array_merge( $columns, [
+                       'rc_title',
+                       'rc_cur_id',
+                       'pp_propname',
+                       'cat_pages',
+                       'cat_subcats',
+                       'cat_files'
+               ] ) );
+               return $it;
+       }
+
+       /**
+        * Fetch newly created categories
+        * @param IDatabase $dbr
+        * @return BatchRowIterator
+        */
+       protected function getNewCatsIterator( IDatabase $dbr ) {
+               $it = $this->setupChangesIterator( $dbr );
+               $it->addConditions( [
+                       'rc_namespace' => NS_CATEGORY,
+                       'rc_new' => 1,
+               ] );
+               return $it;
+       }
+
+       /**
+        * Fetch moved categories
+        * @param IDatabase $dbr
+        * @return BatchRowIterator
+        */
+       protected function getMovedCatsIterator( IDatabase $dbr ) {
+               $it = $this->setupChangesIterator( $dbr, [ 'page_title', 'page_namespace' ], [ 'page' ] );
+               $it->addConditions( [
+                       'rc_namespace' => NS_CATEGORY,
+                       'rc_new' => 0,
+                       'rc_log_type' => 'move',
+                       'rc_type' => RC_LOG,
+               ] );
+               $it->addJoinConditions( [
+                       'page' => [ 'INNER JOIN', 'rc_cur_id = page_id' ],
+               ] );
+               $this->addIndex( $it );
+               return $it;
+       }
+
+       /**
+        * Fetch deleted categories
+        * @param IDatabase $dbr
+        * @return BatchRowIterator
+        */
+       protected function getDeletedCatsIterator( IDatabase $dbr ) {
+               $it = new BatchRowIterator( $dbr,
+                       'recentchanges',
+                       [ 'rc_timestamp' ],
+                       $this->mBatchSize
+               );
+               $this->addTimestampConditions( $it, $dbr );
+               $it->addConditions( [
+                       'rc_namespace' => NS_CATEGORY,
+                       'rc_new' => 0,
+                       'rc_log_type' => 'delete',
+                       'rc_log_action' => 'delete',
+                       'rc_type' => RC_LOG,
+                       // We will fetch ones that do not have page record. If they do,
+                       // this means they were restored, thus restoring handler will pick it up.
+                       'NOT EXISTS (SELECT * FROM page WHERE page_id = rc_cur_id)',
+               ] );
+               $this->addIndex( $it );
+               $it->setFetchColumns( [ 'rc_cur_id', 'rc_title' ] );
+               return $it;
+       }
+
+       /**
+        * Fetch restored categories
+        * @param IDatabase $dbr
+        * @return BatchRowIterator
+        */
+       protected function getRestoredCatsIterator( IDatabase $dbr ) {
+               $it = $this->setupChangesIterator( $dbr );
+               $it->addConditions( [
+                       'rc_namespace' => NS_CATEGORY,
+                       'rc_new' => 0,
+                       'rc_log_type' => 'delete',
+                       'rc_log_action' => 'restore',
+                       'rc_type' => RC_LOG,
+                       // We will only fetch ones that have page record
+                       'EXISTS (SELECT page_id FROM page WHERE page_id = rc_cur_id)',
+               ] );
+               $this->addIndex( $it );
+               return $it;
+       }
+
+       /**
+        * Fetch categorization changes
+        * @param IDatabase $dbr
+        * @return BatchRowIterator
+        */
+       protected function getChangedCatsIterator( IDatabase $dbr ) {
+               $it = $this->setupChangesIterator( $dbr );
+               $it->addConditions( [
+                       'rc_namespace' => NS_CATEGORY,
+                       'rc_new' => 0,
+                       'rc_type' => [ RC_EDIT, RC_CATEGORIZE ],
+               ] );
+               $this->addIndex( $it );
+               return $it;
+       }
+
+       /**
+        * Add timestamp limits to iterator
+        * @param BatchRowIterator $it Iterator
+        * @param IDatabase $dbr
+        */
+       private function addTimestampConditions( BatchRowIterator $it, IDatabase $dbr ) {
+               $it->addConditions( [
+                       'rc_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $this->startTS ) ),
+                       'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $this->endTS ) ),
+               ] );
+       }
+
+       /**
+        * Need to force index, somehow on terbium the optimizer chooses wrong one
+        * @param BatchRowIterator $it
+        */
+       private function addIndex( BatchRowIterator $it ) {
+               $it->addOptions( [
+                       'USE INDEX' => [ 'recentchanges' => 'new_name_timestamp' ]
+               ] );
+       }
+
+       /**
+        * Get iterator for links for categories.
+        * @param IDatabase $dbr
+        * @param array $ids List of page IDs
+        * @return Traversable
+        */
+       protected function getCategoryLinksIterator( IDatabase $dbr, array $ids ) {
+               $it = new BatchRowIterator(
+                       $dbr,
+                       'categorylinks',
+                       [ 'cl_from', 'cl_to' ],
+                       $this->mBatchSize
+               );
+               $it->addConditions( [
+                       'cl_type' => 'subcat',
+                       'cl_from' => $ids
+               ] );
+               $it->setFetchColumns( [ 'cl_from', 'cl_to' ] );
+               return new RecursiveIteratorIterator( $it );
+       }
+
+       /**
+        * Get accumulated RDF.
+        * @return string
+        */
+       public function getRdf() {
+               return $this->rdfWriter->drain();
+       }
+
+       /**
+        * Handle category deletes.
+        * @param IDatabase $dbr
+        * @param resource $output File to write the output
+        */
+       public function handleDeletes( IDatabase $dbr, $output ) {
+               // This only does "true" deletes - i.e. those that the page stays deleted
+               foreach ( $this->getDeletedCatsIterator( $dbr ) as $batch ) {
+                       $deleteUrls = [];
+                       foreach ( $batch as $row ) {
+                               // This can produce duplicates, we don't care
+                               $deleteUrls[] = '<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) . '>';
+                               $this->processed[$row->rc_cur_id] = true;
+                       }
+                       fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, [], "Deletes" ) );
+               }
+       }
+
+       /**
+        * Write category data to RDF.
+        * @param stdclass $row Database row
+        */
+       private function writeCategoryData( $row ) {
+               $this->categoriesRdf->writeCategoryData(
+                       $row->rc_title,
+                       $row->pp_propname === 'hiddencat',
+                       (int)$row->cat_pages - (int)$row->cat_subcats - (int)$row->cat_files,
+                       (int)$row->cat_subcats
+               );
+       }
+
+       /**
+        * @param IDatabase $dbr
+        * @param resource $output
+        */
+       public function handleMoves( IDatabase $dbr, $output ) {
+               foreach ( $this->getMovedCatsIterator( $dbr ) as $batch ) {
+                       $pages = [];
+                       $deleteUrls = [];
+                       foreach ( $batch as $row ) {
+                               $deleteUrls[] = '<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) . '>';
+
+                               if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+                                       // We already captured this one before
+                                       continue;
+                               }
+
+                               if ( $row->page_namespace != NS_CATEGORY ) {
+                                       // If page was moved out of Category:, we'll just delete
+                                       continue;
+                               }
+                               $row->rc_title = $row->page_title;
+                               $this->writeCategoryData( $row );
+                               $pages[$row->rc_cur_id] = $row->page_title;
+                               $this->processed[$row->rc_cur_id] = true;
+                       }
+
+                       fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages, "Moves" ) );
+               }
+       }
+
+       /**
+        * @param IDatabase $dbr
+        * @param resource $output
+        */
+       public function handleRestores( IDatabase $dbr, $output ) {
+               fwrite( $output, "# Restores\n" );
+               // This will only find those restores that were not deleted later.
+               foreach ( $this->getRestoredCatsIterator( $dbr ) as $batch ) {
+                       $pages = [];
+                       foreach ( $batch as $row ) {
+                               if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+                                       // We already captured this one before
+                                       continue;
+                               }
+                               $this->writeCategoryData( $row );
+                               $pages[$row->rc_cur_id] = $row->rc_title;
+                               $this->processed[$row->rc_cur_id] = true;
+                       }
+
+                       if ( empty( $pages ) ) {
+                               continue;
+                       }
+
+                       $this->writeParentCategories( $dbr, $pages );
+
+                       fwrite( $output, sprintf( self::SPARQL_INSERT, $this->getRdf() ) );
+               }
+       }
+
+       /**
+        * @param IDatabase $dbr
+        * @param resource $output
+        */
+       public function handleAdds( IDatabase $dbr, $output ) {
+               fwrite( $output, "# Additions\n" );
+               foreach ( $this->getNewCatsIterator( $dbr ) as $batch ) {
+                       $pages = [];
+                       foreach ( $batch as $row ) {
+                               if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+                                       // We already captured this one before
+                                       continue;
+                               }
+                               $this->writeCategoryData( $row );
+                               $pages[$row->rc_cur_id] = $row->rc_title;
+                               $this->processed[$row->rc_cur_id] = true;
+                       }
+
+                       if ( empty( $pages ) ) {
+                               continue;
+                       }
+
+                       $this->writeParentCategories( $dbr, $pages );
+                       fwrite( $output, sprintf( self::SPARQL_INSERT, $this->getRdf() ) );
+               }
+       }
+
+       /**
+        * @param IDatabase $dbr
+        * @param resource $output
+        */
+       public function handleChanges( IDatabase $dbr, $output ) {
+               foreach ( $this->getChangedCatsIterator( $dbr ) as $batch ) {
+                       $pages = [];
+                       $deleteUrls = [];
+                       foreach ( $batch as $row ) {
+                               if ( isset( $this->processed[$row->rc_cur_id] ) ) {
+                                       // We already captured this one before
+                                       continue;
+                               }
+                               $this->writeCategoryData( $row );
+                               $pages[$row->rc_cur_id] = $row->rc_title;
+                               $this->processed[$row->rc_cur_id] = true;
+                               $deleteUrls[] = '<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) . '>';
+                       }
+
+                       fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages, "Changes" ) );
+               }
+       }
+}
+
+$maintClass = CategoryChangesAsRdf::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
index 2bc0c80..1f23b1a 100644 (file)
     "grunt": "1.0.1",
     "grunt-banana-checker": "0.6.0",
     "grunt-contrib-copy": "1.0.0",
-    "grunt-contrib-watch": "1.0.0",
+    "grunt-contrib-watch": "1.0.1",
     "grunt-eslint": "20.1.0",
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "2.0.0",
     "grunt-stylelint": "0.10.0",
-    "karma": "1.7.1",
+    "karma": "2.0.2",
     "karma-chrome-launcher": "2.2.0",
     "karma-firefox-launcher": "1.0.1",
     "karma-mocha-reporter": "2.2.5",
     "karma-qunit": "2.0.1",
     "postcss-less": "1.1.5",
-    "qunit": "2.5.0",
+    "qunit": "2.6.0",
     "stylelint": "9.2.0",
     "stylelint-config-wikimedia": "0.4.3",
     "wdio-junit-reporter": "0.2.0",
index ea4e5ea..d0bc1ba 100644 (file)
@@ -2001,11 +2001,11 @@ return [
        ],
        'mediawiki.special.apisandbox.styles' => [
                'targets' => [ 'desktop', 'mobile' ],
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css',
+               'styles' => 'resources/src/mediawiki.special.apisandbox.styles.css',
        ],
        'mediawiki.special.apisandbox' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.css',
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.js',
+               'styles' => 'resources/src/mediawiki.special.apisandbox/apisandbox.css',
+               'scripts' => 'resources/src/mediawiki.special.apisandbox/apisandbox.js',
                'targets' => [ 'desktop', 'mobile' ],
                'dependencies' => [
                        'mediawiki.api',
@@ -2073,7 +2073,7 @@ return [
                ],
        ],
        'mediawiki.special.block' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.block.js',
+               'scripts' => 'resources/src/mediawiki.special.block.js',
                'dependencies' => [
                        'oojs-ui-core',
                        'oojs-ui.styles.icons-editing-core',
@@ -2086,7 +2086,7 @@ return [
                ],
        ],
        'mediawiki.special.changecredentials.js' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changecredentials.js',
+               'scripts' => 'resources/src/mediawiki.special.changecredentials.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.htmlform.ooui'
@@ -2094,18 +2094,18 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css',
+               'styles' => 'resources/src/mediawiki.special.changeslist.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist.enhanced' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css',
+               'styles' => 'resources/src/mediawiki.special.changeslist.enhanced.css',
        ],
        'mediawiki.special.changeslist.legend' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css',
+               'styles' => 'resources/src/mediawiki.special.changeslist.legend.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist.legend.js' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js',
+               'scripts' => 'resources/src/mediawiki.special.changeslist.legend.js',
                'dependencies' => [
                        'jquery.makeCollapsible',
                        'mediawiki.cookie',
@@ -2113,20 +2113,20 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.changeslist.visitedstatus' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js',
+               'scripts' => 'resources/src/mediawiki.special.changeslist.visitedstatus.js',
        ],
        'mediawiki.special.comparepages.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less',
+               'styles' => 'resources/src/mediawiki.special.comparepages.styles.less',
        ],
        'mediawiki.special.contributions' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.contributions.js',
+               'scripts' => 'resources/src/mediawiki.special.contributions.js',
                'dependencies' => [
                        'mediawiki.widgets.DateInputWidget',
                        'mediawiki.jqueryMsg',
                ]
        ],
        'mediawiki.special.edittags' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.edittags.js',
+               'scripts' => 'resources/src/mediawiki.special.edittags.js',
                'dependencies' => [
                        'jquery.chosen',
                        'jquery.lengthLimit',
@@ -2137,38 +2137,38 @@ return [
                ],
        ],
        'mediawiki.special.edittags.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.edittags.css',
+               'styles' => 'resources/src/mediawiki.special.edittags.styles.css',
        ],
        'mediawiki.special.import' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.import.js',
+               'scripts' => 'resources/src/mediawiki.special.import.js',
        ],
        'mediawiki.special.movePage' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.movePage.js',
+               'scripts' => 'resources/src/mediawiki.special.movePage.js',
                'dependencies' => [
                        'mediawiki.widgets.visibleLengthLimit',
                        'mediawiki.widgets',
                ],
        ],
        'mediawiki.special.movePage.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.movePage.css',
+               'styles' => 'resources/src/mediawiki.special.movePage.css',
        ],
        'mediawiki.special.pageLanguage' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.pageLanguage.js',
+               'scripts' => 'resources/src/mediawiki.special.pageLanguage.js',
                'dependencies' => [
                        'oojs-ui-core',
                ],
        ],
        'mediawiki.special.pagesWithProp' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
+               'styles' => 'resources/src/mediawiki.special.pagesWithProp.css',
        ],
        'mediawiki.special.preferences' => [
                'targets' => [ 'desktop', 'mobile' ],
                'scripts' => [
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+                       'resources/src/mediawiki.special.preferences/confirmClose.js',
+                       'resources/src/mediawiki.special.preferences/convertmessagebox.js',
+                       'resources/src/mediawiki.special.preferences/tabs.legacy.js',
+                       'resources/src/mediawiki.special.preferences/timezone.js',
+                       'resources/src/mediawiki.special.preferences/personalEmail.js',
                ],
                'messages' => [
                        'prefs-tabs-navigation-hint',
@@ -2184,17 +2184,19 @@ return [
        ],
        'mediawiki.special.preferences.styles' => [
                'targets' => [ 'desktop', 'mobile' ],
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css',
+               // legacy
+               'styles' => 'resources/src/mediawiki.special.preferences.styles.css',
        ],
        'mediawiki.special.preferences.ooui' => [
                'targets' => [ 'desktop', 'mobile' ],
                'scripts' => [
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
-                       'resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js',
+                       // FIXME: This uses files already belonging to another module
+                       'resources/src/mediawiki.special.preferences/confirmClose.js',
+                       'resources/src/mediawiki.special.preferences/convertmessagebox.js',
+                       'resources/src/mediawiki.special.preferences.ooui/editfont.js',
+                       'resources/src/mediawiki.special.preferences.ooui/tabs.js',
+                       'resources/src/mediawiki.special.preferences/timezone.js',
+                       'resources/src/mediawiki.special.preferences/personalEmail.js',
                ],
                'messages' => [
                        'prefs-tabs-navigation-hint',
@@ -2213,14 +2215,14 @@ return [
        ],
        'mediawiki.special.preferences.styles.ooui' => [
                'targets' => [ 'desktop', 'mobile' ],
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
+               'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.css',
        ],
        'mediawiki.special.recentchanges' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.recentchanges.js',
+               'scripts' => 'resources/src/mediawiki.special.recentchanges.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.revisionDelete' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.revisionDelete.js',
+               'scripts' => 'resources/src/mediawiki.special.revisionDelete.js',
                'messages' => [
                        // @todo Load this message in content language
                        'colon-separator',
@@ -2231,8 +2233,8 @@ return [
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.search' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.js',
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.css',
+               'scripts' => 'resources/src/mediawiki.special.search/search.js',
+               'styles' => 'resources/src/mediawiki.special.search/search.css',
                'dependencies' => 'mediawiki.widgets.SearchInputWidget',
                'messages' => [
                        'powersearch-togglelabel',
@@ -2241,7 +2243,7 @@ return [
                ],
        ],
        'mediawiki.special.search.commonsInterwikiWidget' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js',
+               'scripts' => 'resources/src/mediawiki.special.search.commonsInterwikiWidget.js',
                'dependencies' => [
                        'mediawiki.api',
                        'mediawiki.Uri',
@@ -2254,24 +2256,23 @@ return [
                ],
        ],
        'mediawiki.special.search.interwikiwidget.styles' => [
-               'styles' => 'resources/src/mediawiki.special/'
-                       . 'mediawiki.special.search.interwikiwidget.styles.less',
+               'styles' => 'resources/src/mediawiki.special.search.interwikiwidget.styles.less',
                'targets' => [ 'desktop', 'mobile' ]
        ],
        'mediawiki.special.search.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.search.styles.css',
+               'styles' => 'resources/src/mediawiki.special.search.styles.css',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.undelete' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.undelete.js',
+               'scripts' => 'resources/src/mediawiki.special.undelete.js',
                'dependencies' => [
                        'mediawiki.widgets.visibleLengthLimit',
                        'mediawiki.widgets',
                ],
        ],
        'mediawiki.special.unwatchedPages' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js',
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css',
+               'scripts' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js',
+               'styles' => 'resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css',
                'messages' => [
                        'addedwatchtext-short',
                        'removedwatchtext-short',
@@ -2291,9 +2292,9 @@ return [
        ],
        'mediawiki.special.upload' => [
                'templates' => [
-                       'thumbnail.html' => 'resources/src/mediawiki.special/templates/thumbnail.html',
+                       'thumbnail.html' => 'resources/src/mediawiki.special.upload/templates/thumbnail.html',
                ],
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.upload.js',
+               'scripts' => 'resources/src/mediawiki.special.upload/upload.js',
                'messages' => [
                        'widthheight',
                        'size-bytes',
@@ -2319,21 +2320,21 @@ return [
                ],
        ],
        'mediawiki.special.upload.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.upload.styles.css',
+               'styles' => 'resources/src/mediawiki.special.upload.styles.css',
        ],
        'mediawiki.special.userlogin.common.styles' => [
                'targets' => [ 'desktop', 'mobile' ],
                'skinStyles' => [
-                       'default' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.common.css',
+                       'default' => 'resources/src/mediawiki.special.userlogin.common.styles/userlogin.css',
                ],
        ],
        'mediawiki.special.userlogin.login.styles' => [
                'styles' => [
-                       'resources/src/mediawiki.special/mediawiki.special.userlogin.login.css',
+                       'resources/src/mediawiki.special.userlogin.login.styles/login.css',
                ],
        ],
        'mediawiki.special.userlogin.signup.js' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js',
+               'scripts' => 'resources/src/mediawiki.special.userlogin.signup.js',
                'messages' => [
                        'createacct-emailrequired',
                        'noname',
@@ -2348,18 +2349,18 @@ return [
        ],
        'mediawiki.special.userlogin.signup.styles' => [
                'styles' => [
-                       'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css',
+                       'resources/src/mediawiki.special.userlogin.signup.styles/signup.css',
                ],
        ],
        'mediawiki.special.userrights' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userrights.js',
+               'scripts' => 'resources/src/mediawiki.special.userrights.js',
                'dependencies' => [
                        'mediawiki.notification.convertmessagebox',
                        'jquery.lengthLimit',
                ],
        ],
        'mediawiki.special.watchlist' => [
-               'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
+               'scripts' => 'resources/src/mediawiki.special.watchlist.js',
                'messages' => [
                        'addedwatchtext',
                        'addedwatchtext-talk',
@@ -2380,10 +2381,10 @@ return [
                ],
        ],
        'mediawiki.special.watchlist.styles' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.css',
+               'styles' => 'resources/src/mediawiki.special.watchlist.styles.css',
        ],
        'mediawiki.special.version' => [
-               'styles' => 'resources/src/mediawiki.special/mediawiki.special.version.css',
+               'styles' => 'resources/src/mediawiki.special.version.css',
        ],
 
        /* MediaWiki Installer */
index e5cf26e..077473b 100644 (file)
                        if ( !( langData[ langCode ] instanceof mw.Map ) ) {
                                langData[ langCode ] = new mw.Map();
                        }
-                       langData[ langCode ].set( dataKey, value );
+                       if ( arguments.length > 2 ) {
+                               langData[ langCode ].set( dataKey, value );
+                       } else {
+                               langData[ langCode ].set( dataKey );
+                       }
                }
        };
 
index 27ecb1a..d880e8b 100644 (file)
@@ -55,7 +55,7 @@ figure[ typeof*='mw:Audio' ] {
 
        &.mw-halign-right {
                /* @noflip */
-               margin: 0.5em 0 1.3em 1.4em;
+               margin: 0 0 0.5em 0.5em;
                /* @noflip */
                clear: right;
                /* @noflip */
@@ -64,7 +64,7 @@ figure[ typeof*='mw:Audio' ] {
 
        &.mw-halign-left {
                /* @noflip */
-               margin: 0.5em 1.4em 1.3em 0;
+               margin: 0 0.5em 0.5em 0;
                /* @noflip */
                clear: left;
                /* @noflip */
@@ -116,6 +116,15 @@ figure[ typeof~='mw:Audio/Frame' ] {
        clear: right;
        float: right;
 
+       &.mw-halign-left {
+               /* @noflip */
+               margin: 0.5em 1.4em 1.3em 0;
+       }
+       &.mw-halign-right {
+               /* @noflip */
+               margin: 0.5em 0 1.3em 1.4em;
+       }
+
        > *:first-child {
                > img,
                > video {
diff --git a/resources/src/mediawiki.special.apisandbox.styles.css b/resources/src/mediawiki.special.apisandbox.styles.css
new file mode 100644 (file)
index 0000000..4dc4c27
--- /dev/null
@@ -0,0 +1,3 @@
+.client-js .mw-apisandbox-nojs {
+       display: none;
+}
diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.css b/resources/src/mediawiki.special.apisandbox/apisandbox.css
new file mode 100644 (file)
index 0000000..fe5ac41
--- /dev/null
@@ -0,0 +1,110 @@
+.mw-apisandbox-toolbar {
+       background: #fff;
+       -webkit-position: sticky;
+       position: sticky;
+       top: 0;
+       margin-bottom: -1px;
+       padding: 0.5em 0;
+       border-bottom: 1px solid #a2a9b1;
+       text-align: right;
+       z-index: 1;
+}
+
+#mw-apisandbox-ui .mw-apisandbox-link {
+       display: none;
+}
+
+.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget {
+       vertical-align: middle;
+}
+
+/* So DateTimeInputWidget's calendar popup works... */
+.mw-apisandbox-popup .oo-ui-popupWidget-popup,
+.mw-apisandbox-popup .oo-ui-popupWidget-body {
+       overflow: visible;
+}
+
+/* Display contents of the popup on a single line */
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
+       display: table;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
+       display: table-cell;
+}
+
+.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
+       padding-left: 0.5em;
+       width: 1%;
+}
+
+.mw-apisandbox-spacer {
+       display: inline-block;
+       height: 1px;
+       width: 5em;
+}
+
+.mw-apisandbox-help-field {
+       border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
+}
+
+.mw-apisandbox-help-field:last-child {
+       border-bottom: 0;
+}
+
+.mw-apisandbox-optionalWidget {
+       width: 100%;
+}
+
+.mw-apisandbox-optionalWidget.oo-ui-widget-disabled {
+       position: relative;
+       z-index: 0; /* New stacking context to prevent the cover from leaking out */
+}
+
+.mw-apisandbox-optionalWidget-cover {
+       position: absolute;
+       left: 0;
+       right: 0;
+       top: 0;
+       bottom: 0;
+       z-index: 2;
+       cursor: pointer;
+}
+
+.mw-apisandbox-optionalWidget-fields {
+       display: table;
+       width: 100%;
+}
+
+.mw-apisandbox-optionalWidget-widget,
+.mw-apisandbox-optionalWidget-checkbox {
+       display: table-cell;
+       vertical-align: middle;
+}
+
+.mw-apisandbox-optionalWidget-checkbox {
+       width: 1%; /* Will be expanded by content */
+       white-space: nowrap;
+       padding-left: 0.5em;
+}
+
+.mw-apisandbox-textInputCode .oo-ui-inputWidget-input {
+       font-family: monospace, monospace;
+       font-size: 0.8125em;
+       -moz-tab-size: 4;
+       tab-size: 4;
+}
+
+.mw-apisandbox-widget-field .oo-ui-textInputWidget {
+       /* Leave at least enough space for icon, indicator, and a sliver of text */
+       min-width: 6em;
+}
+
+.apihelp-deprecated {
+       font-weight: bold;
+       color: #d33;
+}
+
+.apihelp-deprecated-value .oo-ui-labelElement-label {
+       text-decoration: line-through;
+}
diff --git a/resources/src/mediawiki.special.apisandbox/apisandbox.js b/resources/src/mediawiki.special.apisandbox/apisandbox.js
new file mode 100644 (file)
index 0000000..523a62e
--- /dev/null
@@ -0,0 +1,1864 @@
+( function ( $, mw, OO ) {
+       'use strict';
+       var ApiSandbox, Util, WidgetMethods, Validators,
+               $content, panel, booklet, oldhash, windowManager,
+               formatDropdown,
+               api = new mw.Api(),
+               bookletPages = [],
+               availableFormats = {},
+               resultPage = null,
+               suppressErrors = true,
+               updatingBooklet = false,
+               pages = {},
+               moduleInfoCache = {},
+               baseRequestParams;
+
+       /**
+        * A wrapper for a widget that provides an enable/disable button
+        *
+        * @class
+        * @private
+        * @constructor
+        * @param {OO.ui.Widget} widget
+        * @param {Object} [config] Configuration options
+        */
+       function OptionalWidget( widget, config ) {
+               var k;
+
+               config = config || {};
+
+               this.widget = widget;
+               this.$cover = config.$cover ||
+                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-cover' );
+               this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
+                       .on( 'change', this.onCheckboxChange, [], this );
+
+               OptionalWidget[ 'super' ].call( this, config );
+
+               // Forward most methods for convenience
+               for ( k in this.widget ) {
+                       if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
+                               this[ k ] = this.widget[ k ].bind( this.widget );
+                       }
+               }
+
+               this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
+
+               this.$element
+                       .addClass( 'mw-apisandbox-optionalWidget' )
+                       .append(
+                               this.$cover,
+                               $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
+                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
+                                               widget.$element
+                                       ),
+                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
+                                               this.checkbox.$element
+                                       )
+                               )
+                       );
+
+               this.setDisabled( widget.isDisabled() );
+       }
+       OO.inheritClass( OptionalWidget, OO.ui.Widget );
+       OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
+               this.setDisabled( !checked );
+       };
+       OptionalWidget.prototype.onOverlayClick = function () {
+               this.setDisabled( false );
+               if ( $.isFunction( this.widget.focus ) ) {
+                       this.widget.focus();
+               }
+       };
+       OptionalWidget.prototype.setDisabled = function ( disabled ) {
+               OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
+               this.widget.setDisabled( this.isDisabled() );
+               this.checkbox.setSelected( !this.isDisabled() );
+               this.$cover.toggle( this.isDisabled() );
+               return this;
+       };
+
+       WidgetMethods = {
+               textInputWidget: {
+                       getApiValue: function () {
+                               return this.getValue();
+                       },
+                       setApiValue: function ( v ) {
+                               if ( v === undefined ) {
+                                       v = this.paramInfo[ 'default' ];
+                               }
+                               this.setValue( v );
+                       },
+                       apiCheckValid: function () {
+                               var that = this;
+                               return this.getValidity().then( function () {
+                                       return $.Deferred().resolve( true ).promise();
+                               }, function () {
+                                       return $.Deferred().resolve( false ).promise();
+                               } ).done( function ( ok ) {
+                                       ok = ok || suppressErrors;
+                                       that.setIcon( ok ? null : 'alert' );
+                                       that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               } );
+                       }
+               },
+
+               dateTimeInputWidget: {
+                       getValidity: function () {
+                               if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
+                                       return $.Deferred().resolve().promise();
+                               } else {
+                                       return $.Deferred().reject().promise();
+                               }
+                       }
+               },
+
+               tokenWidget: {
+                       alertTokenError: function ( code, error ) {
+                               windowManager.openWindow( 'errorAlert', {
+                                       title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
+                                       message: error,
+                                       actions: [
+                                               {
+                                                       action: 'accept',
+                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+                                                       flags: 'primary'
+                                               }
+                                       ]
+                               } );
+                       },
+                       fetchToken: function () {
+                               this.pushPending();
+                               return api.getToken( this.paramInfo.tokentype )
+                                       .done( this.setApiValue.bind( this ) )
+                                       .fail( this.alertTokenError.bind( this ) )
+                                       .always( this.popPending.bind( this ) );
+                       },
+                       setApiValue: function ( v ) {
+                               WidgetMethods.textInputWidget.setApiValue.call( this, v );
+                               if ( v === '123ABC' ) {
+                                       this.fetchToken();
+                               }
+                       }
+               },
+
+               passwordWidget: {
+                       getApiValueForDisplay: function () {
+                               return '';
+                       }
+               },
+
+               toggleSwitchWidget: {
+                       getApiValue: function () {
+                               return this.getValue() ? 1 : undefined;
+                       },
+                       setApiValue: function ( v ) {
+                               this.setValue( Util.apiBool( v ) );
+                       },
+                       apiCheckValid: function () {
+                               return $.Deferred().resolve( true ).promise();
+                       }
+               },
+
+               dropdownWidget: {
+                       getApiValue: function () {
+                               var item = this.getMenu().findSelectedItem();
+                               return item === null ? undefined : item.getData();
+                       },
+                       setApiValue: function ( v ) {
+                               var menu = this.getMenu();
+
+                               if ( v === undefined ) {
+                                       v = this.paramInfo[ 'default' ];
+                               }
+                               if ( v === undefined ) {
+                                       menu.selectItem();
+                               } else {
+                                       menu.selectItemByData( String( v ) );
+                               }
+                       },
+                       apiCheckValid: function () {
+                               var ok = this.getApiValue() !== undefined || suppressErrors;
+                               this.setIcon( ok ? null : 'alert' );
+                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               return $.Deferred().resolve( ok ).promise();
+                       }
+               },
+
+               tagWidget: {
+                       getApiValue: function () {
+                               var items = this.getValue();
+                               if ( items.join( '' ).indexOf( '|' ) === -1 ) {
+                                       return items.join( '|' );
+                               } else {
+                                       return '\x1f' + items.join( '\x1f' );
+                               }
+                       },
+                       setApiValue: function ( v ) {
+                               if ( v === undefined || v === '' || v === '\x1f' ) {
+                                       this.setValue( [] );
+                               } else {
+                                       v = String( v );
+                                       if ( v.indexOf( '\x1f' ) !== 0 ) {
+                                               this.setValue( v.split( '|' ) );
+                                       } else {
+                                               this.setValue( v.substr( 1 ).split( '\x1f' ) );
+                                       }
+                               }
+                       },
+                       apiCheckValid: function () {
+                               var ok = true,
+                                       pi = this.paramInfo;
+
+                               if ( !suppressErrors ) {
+                                       ok = this.getApiValue() !== undefined && !(
+                                               pi.allspecifier !== undefined &&
+                                               this.getValue().length > 1 &&
+                                               this.getValue().indexOf( pi.allspecifier ) !== -1
+                                       );
+                               }
+
+                               this.setIcon( ok ? null : 'alert' );
+                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               return $.Deferred().resolve( ok ).promise();
+                       },
+                       createTagItemWidget: function ( data, label ) {
+                               var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
+                               if ( this.paramInfo.deprecatedvalues &&
+                                       this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
+                               ) {
+                                       item.$element.addClass( 'apihelp-deprecated-value' );
+                               }
+                               return item;
+                       }
+               },
+
+               optionalWidget: {
+                       getApiValue: function () {
+                               return this.isDisabled() ? undefined : this.widget.getApiValue();
+                       },
+                       setApiValue: function ( v ) {
+                               this.setDisabled( v === undefined );
+                               this.widget.setApiValue( v );
+                       },
+                       apiCheckValid: function () {
+                               if ( this.isDisabled() ) {
+                                       return $.Deferred().resolve( true ).promise();
+                               } else {
+                                       return this.widget.apiCheckValid();
+                               }
+                       }
+               },
+
+               submoduleWidget: {
+                       single: function () {
+                               var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
+                               return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
+                       },
+                       multi: function () {
+                               var map = this.paramInfo.submodules,
+                                       v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
+                               return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) {
+                                       return { value: v, path: map[ v ] };
+                               } );
+                       }
+               },
+
+               uploadWidget: {
+                       getApiValueForDisplay: function () {
+                               return '...';
+                       },
+                       getApiValue: function () {
+                               return this.getValue();
+                       },
+                       setApiValue: function () {
+                               // Can't, sorry.
+                       },
+                       apiCheckValid: function () {
+                               var ok = this.getValue() !== null || suppressErrors;
+                               this.setIcon( ok ? null : 'alert' );
+                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
+                               return $.Deferred().resolve( ok ).promise();
+                       }
+               }
+       };
+
+       Validators = {
+               generic: function () {
+                       return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
+               }
+       };
+
+       /**
+        * @class mw.special.ApiSandbox.Util
+        * @private
+        */
+       Util = {
+               /**
+                * Fetch API module info
+                *
+                * @param {string} module Module to fetch data for
+                * @return {jQuery.Promise}
+                */
+               fetchModuleInfo: function ( module ) {
+                       var apiPromise,
+                               deferred = $.Deferred();
+
+                       if ( moduleInfoCache.hasOwnProperty( module ) ) {
+                               return deferred
+                                       .resolve( moduleInfoCache[ module ] )
+                                       .promise( { abort: function () {} } );
+                       } else {
+                               apiPromise = api.post( {
+                                       action: 'paraminfo',
+                                       modules: module,
+                                       helpformat: 'html',
+                                       uselang: mw.config.get( 'wgUserLanguage' )
+                               } ).done( function ( data ) {
+                                       var info;
+
+                                       if ( data.warnings && data.warnings.paraminfo ) {
+                                               deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
+                                               return;
+                                       }
+
+                                       info = data.paraminfo.modules;
+                                       if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
+                                               deferred.reject( '???', 'No module data returned' );
+                                               return;
+                                       }
+
+                                       moduleInfoCache[ module ] = info[ 0 ];
+                                       deferred.resolve( info[ 0 ] );
+                               } ).fail( function ( code, details ) {
+                                       if ( code === 'http' ) {
+                                               details = 'HTTP error: ' + details.exception;
+                                       } else if ( details.error ) {
+                                               details = details.error.info;
+                                       }
+                                       deferred.reject( code, details );
+                               } );
+                               return deferred
+                                       .promise( { abort: apiPromise.abort } );
+                       }
+               },
+
+               /**
+                * Mark all currently-in-use tokens as bad
+                */
+               markTokensBad: function () {
+                       var page, subpages, i,
+                               checkPages = [ pages.main ];
+
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+
+                               if ( page.tokenWidget ) {
+                                       api.badToken( page.tokenWidget.paramInfo.tokentype );
+                               }
+
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+               },
+
+               /**
+                * Test an API boolean
+                *
+                * @param {Mixed} value
+                * @return {boolean}
+                */
+               apiBool: function ( value ) {
+                       return value !== undefined && value !== false;
+               },
+
+               /**
+                * Create a widget for a parameter.
+                *
+                * @param {Object} pi Parameter info from API
+                * @param {Object} opts Additional options
+                * @return {OO.ui.Widget}
+                */
+               createWidgetForParameter: function ( pi, opts ) {
+                       var widget, innerWidget, finalWidget, items, $content, func,
+                               multiModeButton = null,
+                               multiModeInput = null,
+                               multiModeAllowed = false;
+
+                       opts = opts || {};
+
+                       switch ( pi.type ) {
+                               case 'boolean':
+                                       widget = new OO.ui.ToggleSwitchWidget();
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.toggleSwitchWidget );
+                                       pi.required = true; // Avoid wrapping in the non-required widget
+                                       break;
+
+                               case 'string':
+                               case 'user':
+                                       if ( Util.apiBool( pi.multi ) ) {
+                                               widget = new OO.ui.TagMultiselectWidget( {
+                                                       allowArbitrary: true,
+                                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.tagWidget );
+                                       } else {
+                                               widget = new OO.ui.TextInputWidget( {
+                                                       required: Util.apiBool( pi.required )
+                                               } );
+                                       }
+                                       if ( !Util.apiBool( pi.multi ) ) {
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.textInputWidget );
+                                               widget.setValidation( Validators.generic );
+                                       }
+                                       if ( pi.tokentype ) {
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.textInputWidget );
+                                               $.extend( widget, WidgetMethods.tokenWidget );
+                                       }
+                                       break;
+
+                               case 'text':
+                                       widget = new OO.ui.MultilineTextInputWidget( {
+                                               required: Util.apiBool( pi.required )
+                                       } );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       widget.setValidation( Validators.generic );
+                                       break;
+
+                               case 'password':
+                                       widget = new OO.ui.TextInputWidget( {
+                                               type: 'password',
+                                               required: Util.apiBool( pi.required )
+                                       } );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       $.extend( widget, WidgetMethods.passwordWidget );
+                                       widget.setValidation( Validators.generic );
+                                       multiModeAllowed = true;
+                                       multiModeInput = widget;
+                                       break;
+
+                               case 'integer':
+                                       widget = new OO.ui.NumberInputWidget( {
+                                               required: Util.apiBool( pi.required ),
+                                               isInteger: true
+                                       } );
+                                       widget.setIcon = widget.input.setIcon.bind( widget.input );
+                                       widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
+                                       widget.getValidity = widget.input.getValidity.bind( widget.input );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       if ( Util.apiBool( pi.enforcerange ) ) {
+                                               widget.setRange( pi.min || -Infinity, pi.max || Infinity );
+                                       }
+                                       multiModeAllowed = true;
+                                       multiModeInput = widget;
+                                       break;
+
+                               case 'limit':
+                                       widget = new OO.ui.TextInputWidget( {
+                                               required: Util.apiBool( pi.required )
+                                       } );
+                                       widget.setValidation( function ( value ) {
+                                               var n, pi = this.paramInfo;
+
+                                               if ( value === 'max' ) {
+                                                       return true;
+                                               } else {
+                                                       n = +value;
+                                                       return !isNaN( n ) && isFinite( n ) &&
+                                                               Math.floor( n ) === n &&
+                                                               n >= pi.min && n <= pi.apiSandboxMax;
+                                               }
+                                       } );
+                                       pi.min = pi.min || 0;
+                                       pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       multiModeAllowed = true;
+                                       multiModeInput = widget;
+                                       break;
+
+                               case 'timestamp':
+                                       widget = new mw.widgets.datetime.DateTimeInputWidget( {
+                                               formatter: {
+                                                       format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
+                                               },
+                                               required: Util.apiBool( pi.required ),
+                                               clearable: false
+                                       } );
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.textInputWidget );
+                                       $.extend( widget, WidgetMethods.dateTimeInputWidget );
+                                       multiModeAllowed = true;
+                                       break;
+
+                               case 'upload':
+                                       widget = new OO.ui.SelectFileWidget();
+                                       widget.paramInfo = pi;
+                                       $.extend( widget, WidgetMethods.uploadWidget );
+                                       break;
+
+                               case 'namespace':
+                                       items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
+                                               if ( ns === '0' ) {
+                                                       name = mw.message( 'blanknamespace' ).text();
+                                               }
+                                               return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
+                                       } ).sort( function ( a, b ) {
+                                               return a.data - b.data;
+                                       } );
+                                       if ( Util.apiBool( pi.multi ) ) {
+                                               if ( pi.allspecifier !== undefined ) {
+                                                       items.unshift( new OO.ui.MenuOptionWidget( {
+                                                               data: pi.allspecifier,
+                                                               label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
+                                                       } ) );
+                                               }
+
+                                               widget = new OO.ui.MenuTagMultiselectWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.tagWidget );
+                                       } else {
+                                               widget = new OO.ui.DropdownWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.dropdownWidget );
+                                       }
+                                       break;
+
+                               default:
+                                       if ( !Array.isArray( pi.type ) ) {
+                                               throw new Error( 'Unknown parameter type ' + pi.type );
+                                       }
+
+                                       items = pi.type.map( function ( v ) {
+                                               var config = {
+                                                       data: String( v ),
+                                                       label: String( v ),
+                                                       classes: []
+                                               };
+                                               if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
+                                                       config.classes.push( 'apihelp-deprecated-value' );
+                                               }
+                                               return new OO.ui.MenuOptionWidget( config );
+                                       } );
+                                       if ( Util.apiBool( pi.multi ) ) {
+                                               if ( pi.allspecifier !== undefined ) {
+                                                       items.unshift( new OO.ui.MenuOptionWidget( {
+                                                               data: pi.allspecifier,
+                                                               label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
+                                                       } ) );
+                                               }
+
+                                               widget = new OO.ui.MenuTagMultiselectWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.tagWidget );
+                                               if ( Util.apiBool( pi.submodules ) ) {
+                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
+                                                       widget.on( 'change', ApiSandbox.updateUI );
+                                               }
+                                       } else {
+                                               widget = new OO.ui.DropdownWidget( {
+                                                       menu: { items: items },
+                                                       $overlay: true
+                                               } );
+                                               widget.paramInfo = pi;
+                                               $.extend( widget, WidgetMethods.dropdownWidget );
+                                               if ( Util.apiBool( pi.submodules ) ) {
+                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.single;
+                                                       widget.getMenu().on( 'select', ApiSandbox.updateUI );
+                                               }
+                                               if ( pi.deprecatedvalues ) {
+                                                       widget.getMenu().on( 'select', function ( item ) {
+                                                               this.$element.toggleClass(
+                                                                       'apihelp-deprecated-value',
+                                                                       pi.deprecatedvalues.indexOf( item.data ) >= 0
+                                                               );
+                                                       }, [], widget );
+                                               }
+                                       }
+
+                                       break;
+                       }
+
+                       if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
+                               innerWidget = widget;
+
+                               multiModeButton = new OO.ui.ButtonWidget( {
+                                       label: mw.message( 'apisandbox-add-multi' ).text()
+                               } );
+                               $content = innerWidget.$element.add( multiModeButton.$element );
+
+                               widget = new OO.ui.PopupTagMultiselectWidget( {
+                                       allowArbitrary: true,
+                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
+                                       $overlay: true,
+                                       popup: {
+                                               classes: [ 'mw-apisandbox-popup' ],
+                                               padded: true,
+                                               $content: $content
+                                       }
+                               } );
+                               widget.paramInfo = pi;
+                               $.extend( widget, WidgetMethods.tagWidget );
+
+                               func = function () {
+                                       if ( !innerWidget.isDisabled() ) {
+                                               innerWidget.apiCheckValid().done( function ( ok ) {
+                                                       if ( ok ) {
+                                                               widget.addTag( innerWidget.getApiValue() );
+                                                               innerWidget.setApiValue( undefined );
+                                                       }
+                                               } );
+                                               return false;
+                                       }
+                               };
+
+                               if ( multiModeInput ) {
+                                       multiModeInput.on( 'enter', func );
+                               }
+                               multiModeButton.on( 'click', func );
+                       }
+
+                       if ( Util.apiBool( pi.required ) || opts.nooptional ) {
+                               finalWidget = widget;
+                       } else {
+                               finalWidget = new OptionalWidget( widget );
+                               finalWidget.paramInfo = pi;
+                               $.extend( finalWidget, WidgetMethods.optionalWidget );
+                               if ( widget.getSubmodules ) {
+                                       finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
+                                       finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
+                               }
+                               finalWidget.setDisabled( true );
+                       }
+
+                       widget.setApiValue( pi[ 'default' ] );
+
+                       return finalWidget;
+               },
+
+               /**
+                * Parse an HTML string and call Util.fixupHTML()
+                *
+                * @param {string} html HTML to parse
+                * @return {jQuery}
+                */
+               parseHTML: function ( html ) {
+                       var $ret = $( $.parseHTML( html ) );
+                       return Util.fixupHTML( $ret );
+               },
+
+               /**
+                * Parse an i18n message and call Util.fixupHTML()
+                *
+                * @param {string} key Key of message to get
+                * @param {...Mixed} parameters Values for $N replacements
+                * @return {jQuery}
+                */
+               parseMsg: function () {
+                       var $ret = mw.message.apply( mw.message, arguments ).parseDom();
+                       return Util.fixupHTML( $ret );
+               },
+
+               /**
+                * Fix HTML for ApiSandbox display
+                *
+                * Fixes are:
+                * - Add target="_blank" to any links
+                *
+                * @param {jQuery} $html DOM to process
+                * @return {jQuery}
+                */
+               fixupHTML: function ( $html ) {
+                       $html.filter( 'a' ).add( $html.find( 'a' ) )
+                               .filter( '[href]:not([target])' )
+                               .attr( 'target', '_blank' );
+                       return $html;
+               },
+
+               /**
+                * Format a request and return a bunch of menu option widgets
+                *
+                * @param {Object} displayParams Query parameters, sanitized for display.
+                * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
+                * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
+                */
+               formatRequest: function ( displayParams, rawParams ) {
+                       var jsonInput,
+                               items = [
+                                       new OO.ui.MenuOptionWidget( {
+                                               label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
+                                               data: new OO.ui.FieldLayout(
+                                                       new OO.ui.TextInputWidget( {
+                                                               readOnly: true,
+                                                               value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams )
+                                                       } ), {
+                                                               label: Util.parseMsg( 'apisandbox-request-url-label' )
+                                                       }
+                                               )
+                                       } ),
+                                       new OO.ui.MenuOptionWidget( {
+                                               label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
+                                               data: new OO.ui.FieldLayout(
+                                                       jsonInput = new OO.ui.MultilineTextInputWidget( {
+                                                               classes: [ 'mw-apisandbox-textInputCode' ],
+                                                               readOnly: true,
+                                                               autosize: true,
+                                                               maxRows: 6,
+                                                               value: JSON.stringify( displayParams, null, '\t' )
+                                                       } ), {
+                                                               label: Util.parseMsg( 'apisandbox-request-json-label' )
+                                                       }
+                                               ).on( 'toggle', function ( visible ) {
+                                                       if ( visible ) {
+                                                               // Call updatePosition instead of adjustSize
+                                                               // because the latter has weird caching
+                                                               // behavior and the former bypasses it.
+                                                               jsonInput.updatePosition();
+                                                       }
+                                               } )
+                                       } )
+                               ];
+
+                       mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams );
+
+                       return items;
+               },
+
+               /**
+                * Event handler for when formatDropdown's selection changes
+                */
+               onFormatDropdownChange: function () {
+                       var i,
+                               menu = formatDropdown.getMenu(),
+                               items = menu.getItems(),
+                               selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null;
+
+                       for ( i = 0; i < items.length; i++ ) {
+                               items[ i ].getData().toggle( items[ i ].getData() === selectedField );
+                       }
+               }
+       };
+
+       /**
+       * Interface to ApiSandbox UI
+       *
+       * @class mw.special.ApiSandbox
+       */
+       ApiSandbox = {
+               /**
+                * Initialize the UI
+                *
+                * Automatically called on $.ready()
+                */
+               init: function () {
+                       var $toolbar;
+
+                       $content = $( '#mw-apisandbox' );
+
+                       windowManager = new OO.ui.WindowManager();
+                       $( 'body' ).append( windowManager.$element );
+                       windowManager.addWindows( {
+                               errorAlert: new OO.ui.MessageDialog()
+                       } );
+
+                       $toolbar = $( '<div>' )
+                               .addClass( 'mw-apisandbox-toolbar' )
+                               .append(
+                                       new OO.ui.ButtonWidget( {
+                                               label: mw.message( 'apisandbox-submit' ).text(),
+                                               flags: [ 'primary', 'progressive' ]
+                                       } ).on( 'click', ApiSandbox.sendRequest ).$element,
+                                       new OO.ui.ButtonWidget( {
+                                               label: mw.message( 'apisandbox-reset' ).text(),
+                                               flags: 'destructive'
+                                       } ).on( 'click', ApiSandbox.resetUI ).$element
+                               );
+
+                       booklet = new OO.ui.BookletLayout( {
+                               expanded: false,
+                               outlined: true,
+                               autoFocus: false
+                       } );
+
+                       panel = new OO.ui.PanelLayout( {
+                               classes: [ 'mw-apisandbox-container' ],
+                               content: [ booklet ],
+                               expanded: false,
+                               framed: true
+                       } );
+
+                       pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
+
+                       // Parse the current hash string
+                       if ( !ApiSandbox.loadFromHash() ) {
+                               ApiSandbox.updateUI();
+                       }
+
+                       $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
+
+                       $content
+                               .empty()
+                               .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
+                               .append(
+                                       $( '<div>' ).attr( 'id', 'mw-apisandbox-ui' )
+                                               .append( $toolbar )
+                                               .append( panel.$element )
+                               );
+               },
+
+               /**
+                * Update the current query when the page hash changes
+                *
+                * @return {boolean} Successful
+                */
+               loadFromHash: function () {
+                       var params, m, re,
+                               hash = location.hash;
+
+                       if ( oldhash === hash ) {
+                               return false;
+                       }
+                       oldhash = hash;
+                       if ( hash === '' ) {
+                               return false;
+                       }
+
+                       // I'm surprised this doesn't seem to exist in jQuery or mw.util.
+                       params = {};
+                       hash = hash.replace( /\+/g, '%20' );
+                       re = /([^&=#]+)=?([^&#]*)/g;
+                       while ( ( m = re.exec( hash ) ) ) {
+                               params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
+                       }
+
+                       ApiSandbox.updateUI( params );
+                       return true;
+               },
+
+               /**
+                * Update the pages in the booklet
+                *
+                * @param {Object} [params] Optional query parameters to load
+                */
+               updateUI: function ( params ) {
+                       var i, page, subpages, j, removePages,
+                               addPages = [];
+
+                       if ( !$.isPlainObject( params ) ) {
+                               params = undefined;
+                       }
+
+                       if ( updatingBooklet ) {
+                               return;
+                       }
+                       updatingBooklet = true;
+                       try {
+                               if ( params !== undefined ) {
+                                       pages.main.loadQueryParams( params );
+                               }
+                               addPages.push( pages.main );
+                               if ( resultPage !== null ) {
+                                       addPages.push( resultPage );
+                               }
+                               pages.main.apiCheckValid();
+
+                               i = 0;
+                               while ( addPages.length ) {
+                                       page = addPages.shift();
+                                       if ( bookletPages[ i ] !== page ) {
+                                               for ( j = i; j < bookletPages.length; j++ ) {
+                                                       if ( bookletPages[ j ].getName() === page.getName() ) {
+                                                               bookletPages.splice( j, 1 );
+                                                       }
+                                               }
+                                               bookletPages.splice( i, 0, page );
+                                               booklet.addPages( [ page ], i );
+                                       }
+                                       i++;
+
+                                       if ( page.getSubpages ) {
+                                               subpages = page.getSubpages();
+                                               for ( j = 0; j < subpages.length; j++ ) {
+                                                       if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
+                                                               subpages[ j ].indentLevel = page.indentLevel + 1;
+                                                               pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
+                                                       }
+                                                       if ( params !== undefined ) {
+                                                               pages[ subpages[ j ].key ].loadQueryParams( params );
+                                                       }
+                                                       addPages.splice( j, 0, pages[ subpages[ j ].key ] );
+                                                       pages[ subpages[ j ].key ].apiCheckValid();
+                                               }
+                                       }
+                               }
+
+                               if ( bookletPages.length > i ) {
+                                       removePages = bookletPages.splice( i, bookletPages.length - i );
+                                       booklet.removePages( removePages );
+                               }
+
+                               if ( !booklet.getCurrentPageName() ) {
+                                       booklet.selectFirstSelectablePage();
+                               }
+                       } finally {
+                               updatingBooklet = false;
+                       }
+               },
+
+               /**
+                * Reset button handler
+                */
+               resetUI: function () {
+                       suppressErrors = true;
+                       pages = {
+                               main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
+                       };
+                       resultPage = null;
+                       ApiSandbox.updateUI();
+               },
+
+               /**
+                * Submit button handler
+                *
+                * @param {Object} [params] Use this set of params instead of those in the form fields.
+                *   The form fields will be updated to match.
+                */
+               sendRequest: function ( params ) {
+                       var page, subpages, i, query, $result, $focus,
+                               progress, $progressText, progressLoading,
+                               deferreds = [],
+                               paramsAreForced = !!params,
+                               displayParams = {},
+                               tokenWidgets = [],
+                               checkPages = [ pages.main ];
+
+                       // Blur any focused widget before submit, because
+                       // OO.ui.ButtonWidget doesn't take focus itself (T128054)
+                       $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
+                       if ( $focus.length ) {
+                               $focus[ 0 ].blur();
+                       }
+
+                       suppressErrors = false;
+
+                       // save widget state in params (or load from it if we are forced)
+                       if ( paramsAreForced ) {
+                               ApiSandbox.updateUI( params );
+                       }
+                       params = {};
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+                               if ( page.tokenWidget ) {
+                                       tokenWidgets.push( page.tokenWidget );
+                               }
+                               deferreds = deferreds.concat( page.apiCheckValid() );
+                               page.getQueryParams( params, displayParams );
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+
+                       if ( !paramsAreForced ) {
+                               // forced params means we are continuing a query; the base query should be preserved
+                               baseRequestParams = $.extend( {}, params );
+                       }
+
+                       $.when.apply( $, deferreds ).done( function () {
+                               var formatItems, menu, selectedLabel, deferred, actions, errorCount;
+
+                               // Count how many times `value` occurs in `array`.
+                               function countValues( value, array ) {
+                                       var count, i;
+                                       count = 0;
+                                       for ( i = 0; i < array.length; i++ ) {
+                                               if ( array[ i ] === value ) {
+                                                       count++;
+                                               }
+                                       }
+                                       return count;
+                               }
+
+                               errorCount = countValues( false, arguments );
+                               if ( errorCount > 0 ) {
+                                       actions = [
+                                               {
+                                                       action: 'accept',
+                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+                                                       flags: 'primary'
+                                               }
+                                       ];
+                                       if ( tokenWidgets.length ) {
+                                               // Check all token widgets' validity separately
+                                               deferred = $.when.apply( $, tokenWidgets.map( function ( w ) {
+                                                       return w.apiCheckValid();
+                                               } ) );
+
+                                               deferred.done( function () {
+                                                       // If only the tokens are invalid, offer to fix them
+                                                       var tokenErrorCount = countValues( false, arguments );
+                                                       if ( tokenErrorCount === errorCount ) {
+                                                               delete actions[ 0 ].flags;
+                                                               actions.push( {
+                                                                       action: 'fix',
+                                                                       label: mw.message( 'apisandbox-results-fixtoken' ).text(),
+                                                                       flags: 'primary'
+                                                               } );
+                                                       }
+                                               } );
+                                       } else {
+                                               deferred = $.Deferred().resolve();
+                                       }
+                                       deferred.always( function () {
+                                               windowManager.openWindow( 'errorAlert', {
+                                                       title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
+                                                       message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
+                                                       actions: actions
+                                               } ).closed.then( function ( data ) {
+                                                       if ( data && data.action === 'fix' ) {
+                                                               ApiSandbox.fixTokenAndResend();
+                                                       }
+                                               } );
+                                       } );
+                                       return;
+                               }
+
+                               query = $.param( displayParams );
+
+                               formatItems = Util.formatRequest( displayParams, params );
+
+                               // Force a 'fm' format with wrappedhtml=1, if available
+                               if ( params.format !== undefined ) {
+                                       if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
+                                               params.format = params.format + 'fm';
+                                       }
+                                       if ( params.format.substr( -2 ) === 'fm' ) {
+                                               params.wrappedhtml = 1;
+                                       }
+                               }
+
+                               progressLoading = false;
+                               $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
+                               progress = new OO.ui.ProgressBarWidget( {
+                                       progress: false,
+                                       $content: $progressText
+                               } );
+
+                               $result = $( '<div>' )
+                                       .append( progress.$element );
+
+                               resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } );
+                               page.setupOutlineItem = function () {
+                                       this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
+                               };
+
+                               if ( !formatDropdown ) {
+                                       formatDropdown = new OO.ui.DropdownWidget( {
+                                               menu: { items: [] },
+                                               $overlay: true
+                                       } );
+                                       formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange );
+                               }
+
+                               menu = formatDropdown.getMenu();
+                               selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : '';
+                               if ( typeof selectedLabel !== 'string' ) {
+                                       selectedLabel = selectedLabel.text();
+                               }
+                               menu.clearItems().addItems( formatItems );
+                               menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() );
+
+                               // Fire the event to update field visibilities
+                               Util.onFormatDropdownChange();
+
+                               page.$element.empty()
+                                       .append(
+                                               new OO.ui.FieldLayout(
+                                                       formatDropdown, {
+                                                               label: Util.parseMsg( 'apisandbox-request-selectformat-label' )
+                                                       }
+                                               ).$element,
+                                               formatItems.map( function ( item ) {
+                                                       return item.getData().$element;
+                                               } ),
+                                               $result
+                                       );
+                               ApiSandbox.updateUI();
+                               booklet.setPage( '|results|' );
+
+                               location.href = oldhash = '#' + query;
+
+                               api.post( params, {
+                                       contentType: 'multipart/form-data',
+                                       dataType: 'text',
+                                       xhr: function () {
+                                               var xhr = new window.XMLHttpRequest();
+                                               xhr.upload.addEventListener( 'progress', function ( e ) {
+                                                       if ( !progressLoading ) {
+                                                               if ( e.lengthComputable ) {
+                                                                       progress.setProgress( e.loaded * 100 / e.total );
+                                                               } else {
+                                                                       progress.setProgress( false );
+                                                               }
+                                                       }
+                                               } );
+                                               xhr.addEventListener( 'progress', function ( e ) {
+                                                       if ( !progressLoading ) {
+                                                               progressLoading = true;
+                                                               $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
+                                                       }
+                                                       if ( e.lengthComputable ) {
+                                                               progress.setProgress( e.loaded * 100 / e.total );
+                                                       } else {
+                                                               progress.setProgress( false );
+                                                       }
+                                               } );
+                                               return xhr;
+                                       }
+                               } )
+                                       .catch( function ( code, data, result, jqXHR ) {
+                                               var deferred = $.Deferred();
+
+                                               if ( code !== 'http' ) {
+                                                       // Not really an error, work around mw.Api thinking it is.
+                                                       deferred.resolve( result, jqXHR );
+                                               } else {
+                                                       // Just forward it.
+                                                       deferred.reject.apply( deferred, arguments );
+                                               }
+                                               return deferred.promise();
+                                       } )
+                                       .then( function ( data, jqXHR ) {
+                                               var m, loadTime, button, clear,
+                                                       ct = jqXHR.getResponseHeader( 'Content-Type' ),
+                                                       loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false';
+
+                                               $result.empty();
+                                               if ( loginSuppressed !== 'false' ) {
+                                                       $( '<div>' )
+                                                               .addClass( 'warning' )
+                                                               .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) )
+                                                               .appendTo( $result );
+                                               }
+                                               if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
+                                                       data = JSON.parse( data );
+                                                       if ( data.modules.length ) {
+                                                               mw.loader.load( data.modules );
+                                                       }
+                                                       if ( data.status && data.status !== 200 ) {
+                                                               $( '<div>' )
+                                                                       .addClass( 'api-pretty-header api-pretty-status' )
+                                                                       .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
+                                                                       .appendTo( $result );
+                                                       }
+                                                       $result.append( Util.parseHTML( data.html ) );
+                                                       loadTime = data.time;
+                                               } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
+                                                       $result.append( Util.parseHTML( m[ 0 ] ) );
+                                                       if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
+                                                               loadTime = parseInt( m[ 1 ], 10 );
+                                                       }
+                                               } else {
+                                                       $( '<pre>' )
+                                                               .addClass( 'api-pretty-content' )
+                                                               .text( data )
+                                                               .appendTo( $result );
+                                               }
+                                               if ( paramsAreForced || data[ 'continue' ] ) {
+                                                       $result.append(
+                                                               $( '<div>' ).append(
+                                                                       new OO.ui.ButtonWidget( {
+                                                                               label: mw.message( 'apisandbox-continue' ).text()
+                                                                       } ).on( 'click', function () {
+                                                                               ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
+                                                                       } ).setDisabled( !data[ 'continue' ] ).$element,
+                                                                       ( clear = new OO.ui.ButtonWidget( {
+                                                                               label: mw.message( 'apisandbox-continue-clear' ).text()
+                                                                       } ).on( 'click', function () {
+                                                                               ApiSandbox.updateUI( baseRequestParams );
+                                                                               clear.setDisabled( true );
+                                                                               booklet.setPage( '|results|' );
+                                                                       } ).setDisabled( !paramsAreForced ) ).$element,
+                                                                       new OO.ui.PopupButtonWidget( {
+                                                                               $overlay: true,
+                                                                               framed: false,
+                                                                               icon: 'info',
+                                                                               popup: {
+                                                                                       $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
+                                                                                       padded: true,
+                                                                                       width: 'auto'
+                                                                               }
+                                                                       } ).$element
+                                                               )
+                                                       );
+                                               }
+                                               if ( typeof loadTime === 'number' ) {
+                                                       $result.append(
+                                                               $( '<div>' ).append(
+                                                                       new OO.ui.LabelWidget( {
+                                                                               label: mw.message( 'apisandbox-request-time', loadTime ).text()
+                                                                       } ).$element
+                                                               )
+                                                       );
+                                               }
+
+                                               if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
+                                                       // Flush all saved tokens in case one of them is the bad one.
+                                                       Util.markTokensBad();
+                                                       button = new OO.ui.ButtonWidget( {
+                                                               label: mw.message( 'apisandbox-results-fixtoken' ).text()
+                                                       } );
+                                                       button.on( 'click', ApiSandbox.fixTokenAndResend )
+                                                               .on( 'click', button.setDisabled, [ true ], button )
+                                                               .$element.appendTo( $result );
+                                               }
+                                       }, function ( code, data ) {
+                                               var details = 'HTTP error: ' + data.exception;
+                                               $result.empty()
+                                                       .append(
+                                                               new OO.ui.LabelWidget( {
+                                                                       label: mw.message( 'apisandbox-results-error', details ).text(),
+                                                                       classes: [ 'error' ]
+                                                               } ).$element
+                                                       );
+                                       } );
+                       } );
+               },
+
+               /**
+                * Handler for the "Correct token and resubmit" button
+                *
+                * Used on a 'badtoken' error, it re-fetches token parameters for all
+                * pages and then re-submits the query.
+                */
+               fixTokenAndResend: function () {
+                       var page, subpages, i, k,
+                               ok = true,
+                               tokenWait = { dummy: true },
+                               checkPages = [ pages.main ],
+                               success = function ( k ) {
+                                       delete tokenWait[ k ];
+                                       if ( ok && $.isEmptyObject( tokenWait ) ) {
+                                               ApiSandbox.sendRequest();
+                                       }
+                               },
+                               failure = function ( k ) {
+                                       delete tokenWait[ k ];
+                                       ok = false;
+                               };
+
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+
+                               if ( page.tokenWidget ) {
+                                       k = page.apiModule + page.tokenWidget.paramInfo.name;
+                                       tokenWait[ k ] = page.tokenWidget.fetchToken();
+                                       tokenWait[ k ]
+                                               .done( success.bind( page.tokenWidget, k ) )
+                                               .fail( failure.bind( page.tokenWidget, k ) );
+                               }
+
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+
+                       success( 'dummy', '' );
+               },
+
+               /**
+                * Reset validity indicators for all widgets
+                */
+               updateValidityIndicators: function () {
+                       var page, subpages, i,
+                               checkPages = [ pages.main ];
+
+                       while ( checkPages.length ) {
+                               page = checkPages.shift();
+                               page.apiCheckValid();
+                               subpages = page.getSubpages();
+                               for ( i = 0; i < subpages.length; i++ ) {
+                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
+                                               checkPages.push( pages[ subpages[ i ].key ] );
+                                       }
+                               }
+                       }
+               }
+       };
+
+       /**
+        * PageLayout for API modules
+        *
+        * @class
+        * @private
+        * @extends OO.ui.PageLayout
+        * @constructor
+        * @param {Object} [config] Configuration options
+        */
+       ApiSandbox.PageLayout = function ( config ) {
+               config = $.extend( { prefix: '', expanded: false }, config );
+               this.displayText = config.key;
+               this.apiModule = config.path;
+               this.prefix = config.prefix;
+               this.paramInfo = null;
+               this.apiIsValid = true;
+               this.loadFromQueryParams = null;
+               this.widgets = {};
+               this.tokenWidget = null;
+               this.indentLevel = config.indentLevel ? config.indentLevel : 0;
+               ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
+               this.loadParamInfo();
+       };
+       OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
+       ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
+               this.outlineItem.setLevel( this.indentLevel );
+               this.outlineItem.setLabel( this.displayText );
+               this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
+               this.outlineItem.setIconTitle(
+                       this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
+               );
+       };
+
+       /**
+        * Fetch module information for this page's module, then create UI
+        */
+       ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
+               var dynamicFieldset, dynamicParamNameWidget,
+                       that = this,
+                       removeDynamicParamWidget = function ( name, layout ) {
+                               dynamicFieldset.removeItems( [ layout ] );
+                               delete that.widgets[ name ];
+                       },
+                       addDynamicParamWidget = function () {
+                               var name, layout, widget, button;
+
+                               // Check name is filled in
+                               name = dynamicParamNameWidget.getValue().trim();
+                               if ( name === '' ) {
+                                       dynamicParamNameWidget.focus();
+                                       return;
+                               }
+
+                               if ( that.widgets[ name ] !== undefined ) {
+                                       windowManager.openWindow( 'errorAlert', {
+                                               title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
+                                               actions: [
+                                                       {
+                                                               action: 'accept',
+                                                               label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
+                                                               flags: 'primary'
+                                                       }
+                                               ]
+                                       } );
+                                       return;
+                               }
+
+                               widget = Util.createWidgetForParameter( {
+                                       name: name,
+                                       type: 'string',
+                                       'default': ''
+                               }, {
+                                       nooptional: true
+                               } );
+                               button = new OO.ui.ButtonWidget( {
+                                       icon: 'trash',
+                                       flags: 'destructive'
+                               } );
+                               layout = new OO.ui.ActionFieldLayout(
+                                       widget,
+                                       button,
+                                       {
+                                               label: name,
+                                               align: 'left'
+                                       }
+                               );
+                               button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
+                               that.widgets[ name ] = widget;
+                               dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
+                               widget.focus();
+
+                               dynamicParamNameWidget.setValue( '' );
+                       };
+
+               this.$element.empty()
+                       .append( new OO.ui.ProgressBarWidget( {
+                               progress: false,
+                               text: mw.message( 'apisandbox-loading', this.displayText ).text()
+                       } ).$element );
+
+               Util.fetchModuleInfo( this.apiModule )
+                       .done( function ( pi ) {
+                               var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
+                                       items = [],
+                                       deprecatedItems = [],
+                                       buttons = [],
+                                       filterFmModules = function ( v ) {
+                                               return v.substr( -2 ) !== 'fm' ||
+                                                       !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
+                                       },
+                                       widgetLabelOnClick = function () {
+                                               var f = this.getField();
+                                               if ( $.isFunction( f.setDisabled ) ) {
+                                                       f.setDisabled( false );
+                                               }
+                                               if ( $.isFunction( f.focus ) ) {
+                                                       f.focus();
+                                               }
+                                       };
+
+                               // This is something of a hack. We always want the 'format' and
+                               // 'action' parameters from the main module to be specified,
+                               // and for 'format' we also want to simplify the dropdown since
+                               // we always send the 'fm' variant.
+                               if ( that.apiModule === 'main' ) {
+                                       for ( i = 0; i < pi.parameters.length; i++ ) {
+                                               if ( pi.parameters[ i ].name === 'action' ) {
+                                                       pi.parameters[ i ].required = true;
+                                                       delete pi.parameters[ i ][ 'default' ];
+                                               }
+                                               if ( pi.parameters[ i ].name === 'format' ) {
+                                                       tmp = pi.parameters[ i ].type;
+                                                       for ( j = 0; j < tmp.length; j++ ) {
+                                                               availableFormats[ tmp[ j ] ] = true;
+                                                       }
+                                                       pi.parameters[ i ].type = tmp.filter( filterFmModules );
+                                                       pi.parameters[ i ][ 'default' ] = 'json';
+                                                       pi.parameters[ i ].required = true;
+                                               }
+                                       }
+                               }
+
+                               // Hide the 'wrappedhtml' parameter on format modules
+                               if ( pi.group === 'format' ) {
+                                       pi.parameters = pi.parameters.filter( function ( p ) {
+                                               return p.name !== 'wrappedhtml';
+                                       } );
+                               }
+
+                               that.paramInfo = pi;
+
+                               items.push( new OO.ui.FieldLayout(
+                                       new OO.ui.Widget( {} ).toggle( false ), {
+                                               align: 'top',
+                                               label: Util.parseHTML( pi.description )
+                                       }
+                               ) );
+
+                               if ( pi.helpurls.length ) {
+                                       buttons.push( new OO.ui.PopupButtonWidget( {
+                                               $overlay: true,
+                                               label: mw.message( 'apisandbox-helpurls' ).text(),
+                                               icon: 'help',
+                                               popup: {
+                                                       width: 'auto',
+                                                       padded: true,
+                                                       $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
+                                                               return $( '<li>' ).append( $( '<a>' )
+                                                                       .attr( { href: link, target: '_blank' } )
+                                                                       .text( link )
+                                                               );
+                                                       } ) )
+                                               }
+                                       } ) );
+                               }
+
+                               if ( pi.examples.length ) {
+                                       buttons.push( new OO.ui.PopupButtonWidget( {
+                                               $overlay: true,
+                                               label: mw.message( 'apisandbox-examples' ).text(),
+                                               icon: 'code',
+                                               popup: {
+                                                       width: 'auto',
+                                                       padded: true,
+                                                       $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
+                                                               var a = $( '<a>' )
+                                                                       .attr( 'href', '#' + example.query )
+                                                                       .html( example.description );
+                                                               a.find( 'a' ).contents().unwrap(); // Can't nest links
+                                                               return $( '<li>' ).append( a );
+                                                       } ) )
+                                               }
+                                       } ) );
+                               }
+
+                               if ( buttons.length ) {
+                                       items.push( new OO.ui.FieldLayout(
+                                               new OO.ui.ButtonGroupWidget( {
+                                                       items: buttons
+                                               } ), { align: 'top' }
+                                       ) );
+                               }
+
+                               if ( pi.parameters.length ) {
+                                       prefix = that.prefix + pi.prefix;
+                                       for ( i = 0; i < pi.parameters.length; i++ ) {
+                                               widget = Util.createWidgetForParameter( pi.parameters[ i ] );
+                                               that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
+                                               if ( pi.parameters[ i ].tokentype ) {
+                                                       that.tokenWidget = widget;
+                                               }
+
+                                               descriptionContainer = $( '<div>' );
+
+                                               tmp = Util.parseHTML( pi.parameters[ i ].description );
+                                               tmp.filter( 'dl' ).makeCollapsible( {
+                                                       collapsed: true
+                                               } ).children( '.mw-collapsible-toggle' ).each( function () {
+                                                       var $this = $( this );
+                                                       $this.parent().prev( 'p' ).append( $this );
+                                               } );
+                                               descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
+
+                                               if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
+                                                       for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
+                                                               descriptionContainer.append( $( '<div>' )
+                                                                       .addClass( 'info' )
+                                                                       .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
+                                                               );
+                                                       }
+                                               }
+                                               flag = true;
+                                               count = 1e100;
+                                               switch ( pi.parameters[ i ].type ) {
+                                                       case 'namespace':
+                                                               flag = false;
+                                                               count = mw.config.get( 'wgFormattedNamespaces' ).length;
+                                                               break;
+
+                                                       case 'limit':
+                                                               if ( pi.parameters[ i ].highmax !== undefined ) {
+                                                                       descriptionContainer.append( $( '<div>' )
+                                                                               .addClass( 'info' )
+                                                                               .append(
+                                                                                       Util.parseMsg(
+                                                                                               'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
+                                                                                       ),
+                                                                                       ' ',
+                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
+                                                                               )
+                                                                       );
+                                                               } else {
+                                                                       descriptionContainer.append( $( '<div>' )
+                                                                               .addClass( 'info' )
+                                                                               .append(
+                                                                                       Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
+                                                                                       ' ',
+                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
+                                                                               )
+                                                                       );
+                                                               }
+                                                               break;
+
+                                                       case 'integer':
+                                                               tmp = '';
+                                                               if ( pi.parameters[ i ].min !== undefined ) {
+                                                                       tmp += 'min';
+                                                               }
+                                                               if ( pi.parameters[ i ].max !== undefined ) {
+                                                                       tmp += 'max';
+                                                               }
+                                                               if ( tmp !== '' ) {
+                                                                       descriptionContainer.append( $( '<div>' )
+                                                                               .addClass( 'info' )
+                                                                               .append( Util.parseMsg(
+                                                                                       'api-help-param-integer-' + tmp,
+                                                                                       Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
+                                                                                       pi.parameters[ i ].min, pi.parameters[ i ].max
+                                                                               ) )
+                                                                       );
+                                                               }
+                                                               break;
+
+                                                       default:
+                                                               if ( Array.isArray( pi.parameters[ i ].type ) ) {
+                                                                       flag = false;
+                                                                       count = pi.parameters[ i ].type.length;
+                                                               }
+                                                               break;
+                                               }
+                                               if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
+                                                       tmp = [];
+                                                       if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
+                                                               !(
+                                                                       widget instanceof OptionalWidget &&
+                                                                       widget.widget instanceof OO.ui.TagMultiselectWidget
+                                                               )
+                                                       ) {
+                                                               tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
+                                                       }
+                                                       if ( count > pi.parameters[ i ].lowlimit ) {
+                                                               tmp.push(
+                                                                       mw.message( 'api-help-param-multi-max',
+                                                                               pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
+                                                                       ).parse()
+                                                               );
+                                                       }
+                                                       if ( tmp.length ) {
+                                                               descriptionContainer.append( $( '<div>' )
+                                                                       .addClass( 'info' )
+                                                                       .append( Util.parseHTML( tmp.join( ' ' ) ) )
+                                                               );
+                                                       }
+                                               }
+                                               if ( 'maxbytes' in pi.parameters[ i ] ) {
+                                                       descriptionContainer.append( $( '<div>' )
+                                                               .addClass( 'info' )
+                                                               .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
+                                                       );
+                                               }
+                                               if ( 'maxchars' in pi.parameters[ i ] ) {
+                                                       descriptionContainer.append( $( '<div>' )
+                                                               .addClass( 'info' )
+                                                               .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
+                                                       );
+                                               }
+                                               helpField = new OO.ui.FieldLayout(
+                                                       new OO.ui.Widget( {
+                                                               $content: '\xa0',
+                                                               classes: [ 'mw-apisandbox-spacer' ]
+                                                       } ), {
+                                                               align: 'inline',
+                                                               classes: [ 'mw-apisandbox-help-field' ],
+                                                               label: descriptionContainer
+                                                       }
+                                               );
+
+                                               layoutConfig = {
+                                                       align: 'left',
+                                                       classes: [ 'mw-apisandbox-widget-field' ],
+                                                       label: prefix + pi.parameters[ i ].name
+                                               };
+
+                                               if ( pi.parameters[ i ].tokentype ) {
+                                                       button = new OO.ui.ButtonWidget( {
+                                                               label: mw.message( 'apisandbox-fetch-token' ).text()
+                                                       } );
+                                                       button.on( 'click', widget.fetchToken, [], widget );
+
+                                                       widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
+                                               } else {
+                                                       widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
+                                               }
+
+                                               // We need our own click handler on the widget label to
+                                               // turn off the disablement.
+                                               widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
+
+                                               // Don't grey out the label when the field is disabled,
+                                               // it makes it too hard to read and our "disabled"
+                                               // isn't really disabled.
+                                               widgetField.onFieldDisable( false );
+                                               widgetField.onFieldDisable = $.noop;
+
+                                               if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
+                                                       deprecatedItems.push( widgetField, helpField );
+                                               } else {
+                                                       items.push( widgetField, helpField );
+                                               }
+                                       }
+                               }
+
+                               if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
+                                       items.push( new OO.ui.FieldLayout(
+                                               new OO.ui.Widget( {} ).toggle( false ), {
+                                                       align: 'top',
+                                                       label: Util.parseMsg( 'apisandbox-no-parameters' )
+                                               }
+                                       ) );
+                               }
+
+                               that.$element.empty();
+
+                               new OO.ui.FieldsetLayout( {
+                                       label: that.displayText
+                               } ).addItems( items )
+                                       .$element.appendTo( that.$element );
+
+                               if ( Util.apiBool( pi.dynamicparameters ) ) {
+                                       dynamicFieldset = new OO.ui.FieldsetLayout();
+                                       dynamicParamNameWidget = new OO.ui.TextInputWidget( {
+                                               placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
+                                       } ).on( 'enter', addDynamicParamWidget );
+                                       dynamicFieldset.addItems( [
+                                               new OO.ui.FieldLayout(
+                                                       new OO.ui.Widget( {} ).toggle( false ), {
+                                                               align: 'top',
+                                                               label: Util.parseHTML( pi.dynamicparameters )
+                                                       }
+                                               ),
+                                               new OO.ui.ActionFieldLayout(
+                                                       dynamicParamNameWidget,
+                                                       new OO.ui.ButtonWidget( {
+                                                               icon: 'add',
+                                                               flags: 'progressive'
+                                                       } ).on( 'click', addDynamicParamWidget ),
+                                                       {
+                                                               label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
+                                                               align: 'left'
+                                                       }
+                                               )
+                                       ] );
+                                       $( '<fieldset>' )
+                                               .append(
+                                                       $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
+                                                       dynamicFieldset.$element
+                                               )
+                                               .appendTo( that.$element );
+                               }
+
+                               if ( deprecatedItems.length ) {
+                                       tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
+                                       $( '<fieldset>' )
+                                               .append(
+                                                       $( '<legend>' ).append(
+                                                               new OO.ui.ToggleButtonWidget( {
+                                                                       label: mw.message( 'apisandbox-deprecated-parameters' ).text()
+                                                               } ).on( 'change', tmp.toggle, [], tmp ).$element
+                                                       ),
+                                                       tmp.$element
+                                               )
+                                               .appendTo( that.$element );
+                               }
+
+                               // Load stored params, if any, then update the booklet if we
+                               // have subpages (or else just update our valid-indicator).
+                               tmp = that.loadFromQueryParams;
+                               that.loadFromQueryParams = null;
+                               if ( $.isPlainObject( tmp ) ) {
+                                       that.loadQueryParams( tmp );
+                               }
+                               if ( that.getSubpages().length > 0 ) {
+                                       ApiSandbox.updateUI( tmp );
+                               } else {
+                                       that.apiCheckValid();
+                               }
+                       } ).fail( function ( code, detail ) {
+                               that.$element.empty()
+                                       .append(
+                                               new OO.ui.LabelWidget( {
+                                                       label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
+                                                       classes: [ 'error' ]
+                                               } ).$element,
+                                               new OO.ui.ButtonWidget( {
+                                                       label: mw.message( 'apisandbox-retry' ).text()
+                                               } ).on( 'click', that.loadParamInfo, [], that ).$element
+                                       );
+                       } );
+       };
+
+       /**
+        * Check that all widgets on the page are in a valid state.
+        *
+        * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
+        */
+       ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
+               var promises, that = this;
+
+               if ( this.paramInfo === null ) {
+                       return [];
+               } else {
+                       promises = $.map( this.widgets, function ( widget ) {
+                               return widget.apiCheckValid();
+                       } );
+                       $.when.apply( $, promises ).then( function () {
+                               that.apiIsValid = $.inArray( false, arguments ) === -1;
+                               if ( that.getOutlineItem() ) {
+                                       that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
+                                       that.getOutlineItem().setIconTitle(
+                                               that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
+                                       );
+                               }
+                       } );
+                       return promises;
+               }
+       };
+
+       /**
+        * Load form fields from query parameters
+        *
+        * @param {Object} params
+        */
+       ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
+               if ( this.paramInfo === null ) {
+                       this.loadFromQueryParams = params;
+               } else {
+                       $.each( this.widgets, function ( name, widget ) {
+                               var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
+                               widget.setApiValue( v );
+                       } );
+               }
+       };
+
+       /**
+        * Load query params from form fields
+        *
+        * @param {Object} params Write query parameters into this object
+        * @param {Object} displayParams Write query parameters for display into this object
+        */
+       ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
+               $.each( this.widgets, function ( name, widget ) {
+                       var value = widget.getApiValue();
+                       if ( value !== undefined ) {
+                               params[ name ] = value;
+                               if ( $.isFunction( widget.getApiValueForDisplay ) ) {
+                                       value = widget.getApiValueForDisplay();
+                               }
+                               displayParams[ name ] = value;
+                       }
+               } );
+       };
+
+       /**
+        * Fetch a list of subpage names loaded by this page
+        *
+        * @return {Array}
+        */
+       ApiSandbox.PageLayout.prototype.getSubpages = function () {
+               var ret = [];
+               $.each( this.widgets, function ( name, widget ) {
+                       var submodules, i;
+                       if ( $.isFunction( widget.getSubmodules ) ) {
+                               submodules = widget.getSubmodules();
+                               for ( i = 0; i < submodules.length; i++ ) {
+                                       ret.push( {
+                                               key: name + '=' + submodules[ i ].value,
+                                               path: submodules[ i ].path,
+                                               prefix: widget.paramInfo.submoduleparamprefix || ''
+                                       } );
+                               }
+                       }
+               } );
+               return ret;
+       };
+
+       $( ApiSandbox.init );
+
+       module.exports = ApiSandbox;
+
+}( jQuery, mediaWiki, OO ) );
diff --git a/resources/src/mediawiki.special.block.js b/resources/src/mediawiki.special.block.js
new file mode 100644 (file)
index 0000000..180f040
--- /dev/null
@@ -0,0 +1,58 @@
+/*!
+ * JavaScript for Special:Block
+ */
+( function ( mw, $ ) {
+       // Like OO.ui.infuse(), but if the element doesn't exist, return null instead of throwing an exception.
+       function infuseOrNull( elem ) {
+               try {
+                       return OO.ui.infuse( elem );
+               } catch ( er ) {
+                       return null;
+               }
+       }
+
+       $( function () {
+               // This code is also loaded on the "block succeeded" page where there is no form,
+               // so username and expiry fields might also be missing.
+               var blockTargetWidget = infuseOrNull( 'mw-bi-target' ),
+                       anonOnlyField = infuseOrNull( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+                       enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+                       hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
+                       watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
+                       expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
+
+               function updateBlockOptions() {
+                       var blocktarget = blockTargetWidget.getValue().trim(),
+                               isEmpty = blocktarget === '',
+                               isIp = mw.util.isIPAddress( blocktarget, true ),
+                               isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
+                               isNonEmptyIp = isIp && !isEmpty,
+                               expiryValue = expiryWidget.getValue(),
+                               // infinityValues  are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
+                               infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
+                               isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
+
+                       if ( enableAutoblockField ) {
+                               enableAutoblockField.toggle( !( isNonEmptyIp ) );
+                       }
+                       if ( hideUserField ) {
+                               hideUserField.toggle( !( isNonEmptyIp || !isIndefinite ) );
+                       }
+                       if ( anonOnlyField ) {
+                               anonOnlyField.toggle( !( !isIp && !isEmpty ) );
+                       }
+                       if ( watchUserField ) {
+                               watchUserField.toggle( !( isIpRange && !isEmpty ) );
+                       }
+               }
+
+               if ( blockTargetWidget ) {
+                       // Bind functions so they're checked whenever stuff changes
+                       blockTargetWidget.on( 'change', updateBlockOptions );
+                       expiryWidget.on( 'change', updateBlockOptions );
+
+                       // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
+                       updateBlockOptions();
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.changecredentials.js b/resources/src/mediawiki.special.changecredentials.js
new file mode 100644 (file)
index 0000000..ad8a4f4
--- /dev/null
@@ -0,0 +1,55 @@
+/*!
+ * JavaScript for change credentials form.
+ */
+( function ( mw, $, OO ) {
+       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+               var api = new mw.Api();
+
+               $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
+                       var currentApiPromise,
+                               self = OO.ui.FieldLayout.static.infuse( $( this ) );
+
+                       self.getField().setValidation( function ( password ) {
+                               var d;
+
+                               if ( currentApiPromise ) {
+                                       currentApiPromise.abort();
+                                       currentApiPromise = undefined;
+                               }
+
+                               password = password.trim();
+
+                               if ( password === '' ) {
+                                       self.setErrors( [] );
+                                       return true;
+                               }
+
+                               d = $.Deferred();
+                               currentApiPromise = api.post( {
+                                       action: 'validatepassword',
+                                       password: password,
+                                       formatversion: 2,
+                                       errorformat: 'html',
+                                       errorsuselocal: true,
+                                       uselang: mw.config.get( 'wgUserLanguage' )
+                               } ).done( function ( resp ) {
+                                       var pwinfo = resp.validatepassword,
+                                               good = pwinfo.validity === 'Good',
+                                               errors = [];
+
+                                       currentApiPromise = undefined;
+
+                                       if ( !good ) {
+                                               pwinfo.validitymessages.map( function ( m ) {
+                                                       errors.push( new OO.ui.HtmlSnippet( m.html ) );
+                                               } );
+                                       }
+                                       self.setErrors( errors );
+                                       d.resolve( good );
+                               } ).fail( d.reject );
+
+                               return d.promise( { abort: currentApiPromise.abort } );
+                       } );
+               } );
+       } );
+}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki.special.changeslist.css b/resources/src/mediawiki.special.changeslist.css
new file mode 100644 (file)
index 0000000..65860ea
--- /dev/null
@@ -0,0 +1,56 @@
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges
+ */
+
+.mw-changeslist-line-watched .mw-title {
+       font-weight: bold;
+}
+
+/*
+ * Titles, including username links, and also tag names
+ * are prone to getting jumbled up
+ * with other titles, usernames, etc. in mixed RTL-LTR environment.
+ */
+.mw-changeslist .mw-tag-marker,
+.mw-changeslist .mw-title {
+       unicode-bidi: embed;
+}
+
+/* Colored watchlist and recent changes numbers */
+.mw-plusminus-pos {
+       color: #006400; /* dark green */
+}
+
+.mw-plusminus-neg {
+       color: #8b0000; /* dark red */
+}
+
+.mw-plusminus-null {
+       color: #a2a9b1; /* gray */
+}
+
+/*
+ * Bidi-isolate these numbers.
+ * See https://phabricator.wikimedia.org/T93484
+ */
+.mw-plusminus-pos,
+.mw-plusminus-neg,
+.mw-plusminus-null {
+       unicode-bidi: -moz-isolate;
+       unicode-bidi: isolate;
+}
+
+/* Prevent FOUC if legend is initially collapsed */
+.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
+       display: none;
+}
+
+.mw-changeslist-legend.mw-collapsed {
+       margin-bottom: 0;
+}
+
+/* Prevent pushing down the content if legend is collapsed */
+.mw-changeslist-legend.mw-collapsed ~ ul:first-of-type > li:first-child,
+.mw-changeslist-legend.mw-collapsed + h4 + div > table.mw-changeslist-line:first-child {
+       clear: right;
+}
diff --git a/resources/src/mediawiki.special.changeslist.enhanced.css b/resources/src/mediawiki.special.changeslist.enhanced.css
new file mode 100644 (file)
index 0000000..cb11332
--- /dev/null
@@ -0,0 +1,69 @@
+/*!
+ * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
+ * a.k.a. Enhanced Recent Changes is enabled.
+ */
+
+table.mw-enhanced-rc {
+       border: 0;
+       border-spacing: 0;
+}
+
+table.mw-enhanced-rc th,
+table.mw-enhanced-rc td {
+       padding: 0;
+       vertical-align: top;
+}
+
+td.mw-enhanced-rc {
+       white-space: nowrap;
+       font-family: monospace, monospace;
+}
+
+.mw-enhanced-rc-time {
+       font-family: monospace, monospace;
+}
+
+table.mw-enhanced-rc td.mw-enhanced-rc-nested {
+       padding-left: 1em;
+}
+
+/* Show/hide arrows in enhanced changeslist */
+.mw-enhanced-rc .collapsible-expander {
+       float: none;
+}
+
+/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
+.client-nojs .mw-enhancedchanges-arrow-space {
+       display: none;
+}
+
+/*
+ * And if it's enabled, let's optimize the collapsing a little: hide the rows
+ * that would be hidden by jquery.makeCollapsible with CSS to save us some
+ * reflows and repaints. This doesn't work on browsers that don't fully support
+ * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
+ * performance instead.
+ */
+.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
+       display: none;
+}
+
+.mw-enhancedchanges-arrow {
+       padding-top: 2px;
+}
+
+.mw-enhancedchanges-arrow-space {
+       display: inline-block;
+       *display: inline; /* IE7 and below */
+       zoom: 1;
+       width: 15px;
+       height: 15px;
+}
+
+.mw-enhanced-watched .mw-enhanced-rc-time {
+       font-weight: bold;
+}
+
+span.changedby {
+       font-size: 95%;
+}
diff --git a/resources/src/mediawiki.special.changeslist.legend.css b/resources/src/mediawiki.special.changeslist.legend.css
new file mode 100644 (file)
index 0000000..14f6aee
--- /dev/null
@@ -0,0 +1,33 @@
+/*!
+ * Styling for changes list legend
+ */
+
+.mw-changeslist-legend {
+       float: right;
+       margin-left: 1em;
+       margin-bottom: 0.5em;
+       clear: right;
+       font-size: 85%;
+       line-height: 1.2em;
+       padding: 0.5em;
+       border: 1px solid #ddd;
+}
+
+.mw-changeslist-legend dl {
+       /* Parent element defines sufficient padding */
+       margin-bottom: 0;
+}
+
+.mw-changeslist-legend dt {
+       float: left;
+       margin: 0 0.5em 0 0;
+}
+
+.mw-changeslist-legend dd {
+       margin-left: 1.5em;
+}
+
+.mw-changeslist-legend dt,
+.mw-changeslist-legend dd {
+       line-height: 1.3em;
+}
diff --git a/resources/src/mediawiki.special.changeslist.legend.js b/resources/src/mediawiki.special.changeslist.legend.js
new file mode 100644 (file)
index 0000000..0792762
--- /dev/null
@@ -0,0 +1,24 @@
+/*!
+ * Script for changes list legend
+ */
+
+/* Remember the collapse state of the legend on recent changes and watchlist pages. */
+( function ( mw ) {
+       var
+               cookieName = 'changeslist-state',
+               // Expanded by default
+               doCollapsibleLegend = function ( $container ) {
+                       $container.find( '.mw-changeslist-legend' )
+                               .makeCollapsible( {
+                                       collapsed: mw.cookie.get( cookieName ) === 'collapsed'
+                               } )
+                               .on( 'beforeExpand.mw-collapsible', function () {
+                                       mw.cookie.set( cookieName, 'expanded' );
+                               } )
+                               .on( 'beforeCollapse.mw-collapsible', function () {
+                                       mw.cookie.set( cookieName, 'collapsed' );
+                               } );
+               };
+
+       mw.hook( 'wikipage.content' ).add( doCollapsibleLegend );
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.special.changeslist.visitedstatus.js b/resources/src/mediawiki.special.changeslist.visitedstatus.js
new file mode 100644 (file)
index 0000000..6b25327
--- /dev/null
@@ -0,0 +1,12 @@
+/*!
+ * JavaScript for Special:Watchlist
+ */
+( function ( $ ) {
+       $( function () {
+               $( '.mw-changeslist-line-watched .mw-title a' ).on( 'click', function () {
+                       $( this )
+                               .closest( '.mw-changeslist-line-watched' )
+                               .removeClass( 'mw-changeslist-line-watched' );
+               } );
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.comparepages.styles.less b/resources/src/mediawiki.special.comparepages.styles.less
new file mode 100644 (file)
index 0000000..87b7a8b
--- /dev/null
@@ -0,0 +1,19 @@
+@import 'mediawiki.mixins';
+
+.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
+       width: 100%;
+}
+
+.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
+       float: left;
+       width: 49%;
+       .box-sizing( border-box );
+}
+
+.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed:nth-of-type( 2 ) {
+       margin-left: 2%;
+}
+
+.mw-special-ComparePages .mw-htmlform-submit-buttons {
+       clear: both;
+}
diff --git a/resources/src/mediawiki.special.contributions.js b/resources/src/mediawiki.special.contributions.js
new file mode 100644 (file)
index 0000000..f65a257
--- /dev/null
@@ -0,0 +1,12 @@
+( function ( mw, $ ) {
+       $( function () {
+               var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
+                       endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
+
+               startInput.on( 'deactivate', function ( userSelected ) {
+                       if ( userSelected ) {
+                               endInput.focus();
+                       }
+               } );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.edittags.js b/resources/src/mediawiki.special.edittags.js
new file mode 100644 (file)
index 0000000..4f51e9b
--- /dev/null
@@ -0,0 +1,38 @@
+/*!
+ * JavaScript for Special:EditTags
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+                       $wpReason = $( '#wpReason' ),
+                       $tagList = $( '#mw-edittags-tag-list' );
+
+               if ( $tagList.length ) {
+                       $tagList.chosen( {
+                               /* eslint-disable camelcase */
+                               placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
+                               no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
+                               /* eslint-enable camelcase */
+                       } );
+               }
+
+               $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
+                       $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
+               } );
+               $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
+                       if ( !e.target.checked ) {
+                               $( '#mw-edittags-remove-all' ).prop( 'checked', false );
+                       }
+               } );
+
+               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+               // use maxLength because it's leaving room for log entry text.
+               if ( summaryCodePointLimit ) {
+                       $wpReason.codePointLimit();
+               } else if ( summaryByteLimit ) {
+                       $wpReason.byteLimit();
+               }
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.edittags.styles.css b/resources/src/mediawiki.special.edittags.styles.css
new file mode 100644 (file)
index 0000000..204009c
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for Special:EditTags and action=editchangetags
+ */
+#mw-edittags-tags-selector td {
+       vertical-align: top;
+}
+
+#mw-edittags-tags-selector-multi td {
+       vertical-align: top;
+       padding-right: 1.5em;
+}
+
+#mw-edittags-tag-list {
+       min-width: 20em;
+}
diff --git a/resources/src/mediawiki.special.import.js b/resources/src/mediawiki.special.import.js
new file mode 100644 (file)
index 0000000..2cb96af
--- /dev/null
@@ -0,0 +1,37 @@
+/*!
+ * JavaScript for Special:Import
+ */
+( function ( $ ) {
+       var subprojectListAlreadyShown;
+       function updateImportSubprojectList() {
+               var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
+                       $subprojectField = $projectField.parent().find( '#subproject' ),
+                       $selected = $projectField.find( ':selected' ),
+                       oldValue = $subprojectField.val(),
+                       option, options;
+
+               if ( $selected.attr( 'data-subprojects' ) ) {
+                       options = $selected.attr( 'data-subprojects' ).split( ' ' ).map( function ( el ) {
+                               option = document.createElement( 'option' );
+                               option.appendChild( document.createTextNode( el ) );
+                               option.setAttribute( 'value', el );
+                               if ( oldValue === el && subprojectListAlreadyShown === true ) {
+                                       option.setAttribute( 'selected', 'selected' );
+                               }
+                               return option;
+                       } );
+                       $subprojectField.show().empty().append( options );
+                       subprojectListAlreadyShown = true;
+               } else {
+                       $subprojectField.hide();
+               }
+       }
+
+       $( function () {
+               var $projectField = $( '#mw-import-table-interwiki #interwiki' );
+               if ( $projectField.length ) {
+                       $projectField.change( updateImportSubprojectList );
+                       updateImportSubprojectList();
+               }
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.movePage.css b/resources/src/mediawiki.special.movePage.css
new file mode 100644 (file)
index 0000000..9428fed
--- /dev/null
@@ -0,0 +1,7 @@
+/*!
+ * Styles for Special:MovePage
+ */
+
+.movepage-wrapper {
+       width: 50em;
+}
diff --git a/resources/src/mediawiki.special.movePage.js b/resources/src/mediawiki.special.movePage.js
new file mode 100644 (file)
index 0000000..d828396
--- /dev/null
@@ -0,0 +1,23 @@
+/*!
+ * JavaScript for Special:MovePage
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+                       wpReason = OO.ui.infuse( $( '#wpReason' ) );
+
+               // Infuse for pretty dropdown
+               OO.ui.infuse( $( '#wpNewTitle' ) );
+               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+               if ( summaryCodePointLimit ) {
+                       mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
+               } else if ( summaryByteLimit ) {
+                       mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
+               }
+               // Infuse for nicer "help" popup
+               if ( $( '#wpMovetalk-field' ).length ) {
+                       OO.ui.infuse( $( '#wpMovetalk-field' ) );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special.pageLanguage.js
new file mode 100644 (file)
index 0000000..edfbe1e
--- /dev/null
@@ -0,0 +1,11 @@
+/*!
+ * JavaScript module used on Special:PageLanguage
+ */
+( function ( $, OO ) {
+       $( function () {
+               // Select the 'Language select' option if user is trying to select language
+               OO.ui.infuse( 'mw-pl-languageselector' ).on( 'change', function () {
+                       OO.ui.infuse( 'mw-pl-options' ).setValue( '2' );
+               } );
+       } );
+}( jQuery, OO ) );
diff --git a/resources/src/mediawiki.special.pagesWithProp.css b/resources/src/mediawiki.special.pagesWithProp.css
new file mode 100644 (file)
index 0000000..7ef75d0
--- /dev/null
@@ -0,0 +1,4 @@
+/* Distinguish actual data from information about it being hidden visually */
+.prop-value-hidden {
+       font-style: italic;
+}
diff --git a/resources/src/mediawiki.special.preferences.ooui/editfont.js b/resources/src/mediawiki.special.preferences.ooui/editfont.js
new file mode 100644 (file)
index 0000000..fe48886
--- /dev/null
@@ -0,0 +1,32 @@
+/*!
+ * JavaScript for Special:Preferences: editfont field enhancements.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var widget, lastValue;
+
+               try {
+                       widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       return;
+               }
+
+               // Style options
+               widget.dropdownWidget.menu.items.forEach( function ( item ) {
+                       item.$label.addClass( 'mw-editfont-' + item.getData() );
+               } );
+
+               function updateLabel( value ) {
+                       // Style selected item label
+                       widget.dropdownWidget.$label
+                               .removeClass( 'mw-editfont-' + lastValue )
+                               .addClass( 'mw-editfont-' + value );
+                       lastValue = value;
+               }
+
+               widget.on( 'change', updateLabel );
+               updateLabel( widget.getValue() );
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences.ooui/tabs.js b/resources/src/mediawiki.special.preferences.ooui/tabs.js
new file mode 100644 (file)
index 0000000..c948ff0
--- /dev/null
@@ -0,0 +1,138 @@
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $preferences, tabs, wrapper, previousTab;
+
+               $preferences = $( '#preferences' );
+
+               // Make sure the accessibility tip is selectable so that screen reader users take notice,
+               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+               // when selected. Similar to jquery.mw-jump
+               $( '<div>' ).addClass( 'mw-navigation-hint' )
+                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+                       .attr( 'tabIndex', 0 )
+                       .on( 'focus blur', function ( e ) {
+                               if ( e.type === 'blur' || e.type === 'focusout' ) {
+                                       $( this ).css( 'height', '0' );
+                               } else {
+                                       $( this ).css( 'height', 'auto' );
+                               }
+                       } ).prependTo( '#mw-content-text' );
+
+               tabs = new OO.ui.IndexLayout( {
+                       expanded: false,
+                       // Do not remove focus from the tabs menu after choosing a tab
+                       autoFocus: false
+               } );
+
+               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+                       var panel, $panelContents;
+
+                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+                               expanded: false,
+                               label: tabConfig.label
+                       } );
+                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+                       // Hide the unnecessary PHP PanelLayouts
+                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
+                       $panelContents.parent().detach();
+
+                       panel.$element.append( $panelContents );
+                       tabs.addTabPanels( [ panel ] );
+
+                       // Remove duplicate labels
+                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+                       $panelContents.children( 'legend' ).remove();
+                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+               } );
+
+               wrapper = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       padded: false,
+                       framed: true
+               } );
+               wrapper.$element.append( tabs.$element );
+               $preferences.prepend( wrapper.$element );
+
+               function updateHash( panel ) {
+                       var scrollTop, active;
+                       // Handle hash manually to prevent jumping,
+                       // therefore save and restore scrollTop to prevent jumping.
+                       scrollTop = $( window ).scrollTop();
+                       // Changing the hash apparently causes keyboard focus to be lost?
+                       // Save and restore it. This makes no sense though.
+                       active = document.activeElement;
+                       location.hash = '#mw-prefsection-' + panel.getName();
+                       if ( active ) {
+                               active.focus();
+                       }
+                       $( window ).scrollTop( scrollTop );
+               }
+
+               tabs.on( 'set', updateHash );
+
+               /**
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to supress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       if ( mode === 'noHash' ) {
+                               tabs.off( 'set', updateHash );
+                       }
+                       tabs.setTabPanel( name );
+                       if ( mode === 'noHash' ) {
+                               tabs.on( 'set', updateHash );
+                       }
+               }
+
+               // Jump to correct section as indicated by the hash.
+               // This function is called onload and onhashchange.
+               function detectHash() {
+                       var hash = location.hash,
+                               matchedElement, parentSection;
+                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+                               mw.storage.session.remove( 'mwpreferences-prevTab' );
+                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+                               matchedElement = document.getElementById( hash.slice( 1 ) );
+                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+                               if ( parentSection.length ) {
+                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+                                       // Switch to proper tab and scroll to selected item.
+                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+                                       matchedElement.scrollIntoView();
+                               }
+                       }
+               }
+
+               $( window ).on( 'hashchange', function () {
+                       var hash = location.hash;
+                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
+                               detectHash();
+                       } else if ( hash === '' ) {
+                               switchPrefTab( 'personal', 'noHash' );
+                       }
+               } )
+                       // Run the function immediately to select the proper tab on startup.
+                       .trigger( 'hashchange' );
+
+               // Restore the active tab after saving the preferences
+               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+               if ( previousTab ) {
+                       switchPrefTab( previousTab, 'noHash' );
+                       // Deleting the key, the tab states should be reset until we press Save
+                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+               }
+
+               $( '#mw-prefs-form' ).on( 'submit', function () {
+                       var value = tabs.getCurrentTabPanelName();
+                       mw.storage.session.set( 'mwpreferences-prevTab', value );
+               } );
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences.styles.css b/resources/src/mediawiki.special.preferences.styles.css
new file mode 100644 (file)
index 0000000..33b630a
--- /dev/null
@@ -0,0 +1,47 @@
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .mw-input,
+.mw-email-none .mw-input {
+       border: 1px solid #fde29b;
+       background-color: #fdf1d1;
+       color: #000;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .mw-input { }
+*/
+/* This breaks due to nolabel styling */
+#preferences > fieldset td.mw-label {
+       width: 20%;
+}
+
+#preferences > fieldset table {
+       width: 100%;
+}
+#preferences > fieldset table.mw-htmlform-matrix {
+       width: auto;
+}
+
+/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+       overflow: hidden;
+       height: 0;
+       zoom: 1;
+}
+
+.client-nojs #preftoc {
+       display: none;
+}
+
+.client-js #preferences > fieldset {
+       display: none;
+}
+
+/* Only the 1st tab is shown by default in JS mode */
+.client-js #preferences #mw-prefsection-personal {
+       display: block;
+}
diff --git a/resources/src/mediawiki.special.preferences.styles.ooui.css b/resources/src/mediawiki.special.preferences.styles.ooui.css
new file mode 100644 (file)
index 0000000..8810318
--- /dev/null
@@ -0,0 +1,118 @@
+/* Reuses colors from mediawiki.legacy/shared.css */
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
+       border: 1px solid #fde29b;
+       background-color: #fdf1d1;
+       color: #000;
+       padding: 0.5em;
+}
+/* Authenticated email field has its own class too. Unstyled by default */
+/*
+.mw-email-authenticated .oo-ui-labelWidget { }
+*/
+
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+       margin: 0;
+       display: inline;
+}
+
+.mw-prefs-buttons {
+       margin-top: 1em;
+}
+
+#prefcontrol {
+       margin-right: 0.5em;
+}
+
+/*
+ * Hide, but keep accessible for screen-readers.
+ * Like .mw-jump, #jump-to-nav from shared.css
+ */
+.client-js .mw-navigation-hint {
+       overflow: hidden;
+       height: 0;
+       zoom: 1;
+}
+
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+       position: static;
+       overflow: visible;
+       -webkit-transform: none;
+       transform: none;
+}
+
+#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       border-color: #c8ccd1;
+       border-width: 1px 0 0;
+       border-radius: 0;
+       padding-left: 0;
+       padding-right: 0;
+       box-shadow: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+       margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout {
+       padding-top: 0.5em;
+       padding-bottom: 0.5em;
+}
+
+.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+       margin-left: 0;
+       margin-bottom: 0;
+       border: 0;
+       padding-top: 0;
+}
+
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+       margin-bottom: 1em;
+}
+
+/* Make the "Basic information" section more compact */
+/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
+       width: 20%;
+       display: inline-block;
+       vertical-align: middle;
+       padding: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
+       margin-right: 0;
+}
+
+#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
+       width: 80%;
+       display: inline-block;
+       vertical-align: middle;
+}
+
+/* Expand the dropdown and textfield of "Time zone" field to the */
+/* usual maximum width and display them on separate lines. */
+#wpTimeCorrection .oo-ui-dropdownInputWidget,
+#wpTimeCorrection .oo-ui-textInputWidget {
+       display: block;
+       max-width: 50em;
+}
+
+#wpTimeCorrection .oo-ui-textInputWidget {
+       margin-top: 0.5em;
+}
+
+/* HACK: expand width of gadget descriptions.
+ * This should be moved to the Gadgets extension */
+#mw-htmlform-gadgets .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
+       max-width: none;
+}
diff --git a/resources/src/mediawiki.special.preferences/confirmClose.js b/resources/src/mediawiki.special.preferences/confirmClose.js
new file mode 100644 (file)
index 0000000..244154b
--- /dev/null
@@ -0,0 +1,86 @@
+/*!
+ * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
+ * closed when any form field is changed.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var allowCloseWindow, saveButton, restoreButton,
+                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
+
+               // Check if all of the form values are unchanged.
+               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+               // slower and more complicated. It works fine to treat them as HTML elements.)
+               function isPrefsChanged() {
+                       var inputs = $( '#mw-prefs-form :input[name]' ),
+                               input, $input, inputType,
+                               index, optIndex,
+                               opt;
+
+                       for ( index = 0; index < inputs.length; index++ ) {
+                               input = inputs[ index ];
+                               $input = $( input );
+
+                               // Different types of inputs have different methods for accessing defaults
+                               if ( $input.is( 'select' ) ) {
+                                       // <select> has the property defaultSelected for each option
+                                       for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
+                                               opt = input.options[ optIndex ];
+                                               if ( opt.selected !== opt.defaultSelected ) {
+                                                       return true;
+                                               }
+                                       }
+                               } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
+                                       // <input> has defaultValue or defaultChecked
+                                       inputType = input.type;
+                                       if ( inputType === 'radio' || inputType === 'checkbox' ) {
+                                               if ( input.checked !== input.defaultChecked ) {
+                                                       return true;
+                                               }
+                                       } else if ( input.value !== input.defaultValue ) {
+                                               return true;
+                                       }
+                               }
+                       }
+
+                       return false;
+               }
+
+               if ( oouiEnabled ) {
+                       saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+                       restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
+                       // Disable the button to save preferences unless preferences have changed
+                       // Check if preferences have been changed before JS has finished loading
+                       saveButton.setDisabled( !isPrefsChanged() );
+                       $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
+                               saveButton.setDisabled( !isPrefsChanged() );
+                       } );
+               } else {
+                       // Disable the button to save preferences unless preferences have changed
+                       // Check if preferences have been changed before JS has finished loading
+                       $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+                       $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
+                               $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
+                       } );
+               }
+
+               // Set up a message to notify users if they try to leave the page without
+               // saving.
+               allowCloseWindow = mw.confirmCloseWindow( {
+                       test: isPrefsChanged,
+                       message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
+                       namespace: 'prefswarning'
+               } );
+               $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
+               if ( oouiEnabled ) {
+                       restoreButton.on( 'click', function () {
+                               allowCloseWindow.release();
+                               // The default behavior of events in OOUI is always prevented. Follow the link manually.
+                               // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+                               location.href = restoreButton.getHref();
+                       } );
+               } else {
+                       $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/convertmessagebox.js b/resources/src/mediawiki.special.preferences/convertmessagebox.js
new file mode 100644 (file)
index 0000000..e6b7432
--- /dev/null
@@ -0,0 +1,9 @@
+/*!
+ * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
+ */
+( function ( $ ) {
+       $( function () {
+               var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
+               convertmessagebox();
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/personalEmail.js b/resources/src/mediawiki.special.preferences/personalEmail.js
new file mode 100644 (file)
index 0000000..f934d59
--- /dev/null
@@ -0,0 +1,24 @@
+/*!
+ * JavaScript for Special:Preferences: Email preferences better UX
+ */
+( function ( $ ) {
+       $( function () {
+               var allowEmail, allowEmailFromNewUsers;
+
+               allowEmail = $( '#wpAllowEmail' );
+               allowEmailFromNewUsers = $( '#wpAllowEmailFromNewUsers' );
+
+               function toggleDisabled() {
+                       if ( allowEmail.is( ':checked' ) && allowEmail.is( ':enabled' ) ) {
+                               allowEmailFromNewUsers.prop( 'disabled', false );
+                       } else {
+                               allowEmailFromNewUsers.prop( 'disabled', true );
+                       }
+               }
+
+               if ( allowEmail ) {
+                       allowEmail.on( 'change', toggleDisabled );
+                       toggleDisabled();
+               }
+       } );
+}( jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/tabs.legacy.js b/resources/src/mediawiki.special.preferences/tabs.legacy.js
new file mode 100644 (file)
index 0000000..0d97d68
--- /dev/null
@@ -0,0 +1,143 @@
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+
+               labelFunc = function () {
+                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
+               };
+
+               $preftoc = $( '#preftoc' );
+               $preferences = $( '#preferences' );
+
+               $fieldsets = $preferences.children( 'fieldset' )
+                       .attr( {
+                               role: 'tabpanel',
+                               'aria-labelledby': labelFunc
+                       } );
+               $fieldsets.not( '#mw-prefsection-personal' )
+                       .hide()
+                       .attr( 'aria-hidden', 'true' );
+
+               // T115692: The following is kept for backwards compatibility with older skins
+               $preferences.addClass( 'jsprefs' );
+               $fieldsets.addClass( 'prefsection' );
+               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
+
+               // Make sure the accessibility tip is selectable so that screen reader users take notice,
+               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+               // when selected. Similar to jquery.mw-jump
+               $( '<div>' ).addClass( 'mw-navigation-hint' )
+                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+                       .attr( 'tabIndex', 0 )
+                       .on( 'focus blur', function ( e ) {
+                               if ( e.type === 'blur' || e.type === 'focusout' ) {
+                                       $( this ).css( 'height', '0' );
+                               } else {
+                                       $( this ).css( 'height', 'auto' );
+                               }
+                       } ).insertBefore( $preftoc );
+
+               /**
+                * It uses document.getElementById for security reasons (HTML injections in $()).
+                *
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to surpress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       var $tab, scrollTop;
+                       // Handle hash manually to prevent jumping,
+                       // therefore save and restore scrollTop to prevent jumping.
+                       scrollTop = $( window ).scrollTop();
+                       if ( mode !== 'noHash' ) {
+                               location.hash = '#mw-prefsection-' + name;
+                       }
+                       $( window ).scrollTop( scrollTop );
+
+                       $preftoc.find( 'li' ).removeClass( 'selected' )
+                               .find( 'a' ).attr( {
+                                       tabIndex: -1,
+                                       'aria-selected': 'false'
+                               } );
+
+                       $tab = $( document.getElementById( 'preftab-' + name ) );
+                       if ( $tab.length ) {
+                               $tab.attr( {
+                                       tabIndex: 0,
+                                       'aria-selected': 'true'
+                               } ).focus()
+                                       .parent().addClass( 'selected' );
+
+                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
+                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
+                       }
+               }
+
+               // Enable keyboard users to use left and right keys to switch tabs
+               $preftoc.on( 'keydown', function ( event ) {
+                       var keyLeft = 37,
+                               keyRight = 39,
+                               $el;
+
+                       if ( event.keyCode === keyLeft ) {
+                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
+                       } else if ( event.keyCode === keyRight ) {
+                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
+                       } else {
+                               return;
+                       }
+                       if ( $el.length > 0 ) {
+                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       }
+               } );
+
+               // Jump to correct section as indicated by the hash.
+               // This function is called onload and onhashchange.
+               function detectHash() {
+                       var hash = location.hash,
+                               matchedElement, parentSection;
+                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+                               mw.storage.session.remove( 'mwpreferences-prevTab' );
+                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+                               matchedElement = document.getElementById( hash.slice( 1 ) );
+                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+                               if ( parentSection.length ) {
+                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+                                       // Switch to proper tab and scroll to selected item.
+                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+                                       matchedElement.scrollIntoView();
+                               }
+                       }
+               }
+
+               $( window ).on( 'hashchange', function () {
+                       var hash = location.hash;
+                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
+                               detectHash();
+                       } else if ( hash === '' ) {
+                               switchPrefTab( 'personal', 'noHash' );
+                       }
+               } )
+                       // Run the function immediately to select the proper tab on startup.
+                       .trigger( 'hashchange' );
+
+               // Restore the active tab after saving the preferences
+               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+               if ( previousTab ) {
+                       switchPrefTab( previousTab, 'noHash' );
+                       // Deleting the key, the tab states should be reset until we press Save
+                       mw.storage.session.remove( 'mwpreferences-prevTab' );
+               }
+
+               $( '#mw-prefs-form' ).on( 'submit', function () {
+                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       mw.storage.session.set( 'mwpreferences-prevTab', value );
+               } );
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.preferences/timezone.js b/resources/src/mediawiki.special.preferences/timezone.js
new file mode 100644 (file)
index 0000000..a6ffae9
--- /dev/null
@@ -0,0 +1,111 @@
+/*!
+ * JavaScript for Special:Preferences: Timezone field enhancements.
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
+                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
+
+               // Timezone functions.
+               // Guesses Timezone from browser and updates fields onchange.
+
+               if ( oouiEnabled ) {
+                       // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+                       try {
+                               timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+                       } catch ( err ) {
+                               // This preference could theoretically be disabled ($wgHiddenPrefs)
+                               timezoneWidget = null;
+                       }
+               } else {
+                       $tzSelect = $( '#mw-input-wptimecorrection' );
+                       $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               }
+
+               $localtimeHolder = $( '#wpLocalTime' );
+               servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
+
+               function minutesToHours( min ) {
+                       var tzHour = Math.floor( Math.abs( min ) / 60 ),
+                               tzMin = Math.abs( min ) % 60,
+                               tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
+                                       ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
+                       return tzString;
+               }
+
+               function hoursToMinutes( hour ) {
+                       var minutes,
+                               arr = hour.split( ':' );
+
+                       arr[ 0 ] = parseInt( arr[ 0 ], 10 );
+
+                       if ( arr.length === 1 ) {
+                               // Specification is of the form [-]XX
+                               minutes = arr[ 0 ] * 60;
+                       } else {
+                               // Specification is of the form [-]XX:XX
+                               minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
+                               if ( arr[ 0 ] < 0 ) {
+                                       minutes *= -1;
+                               }
+                       }
+                       // Gracefully handle non-numbers.
+                       if ( isNaN( minutes ) ) {
+                               return 0;
+                       } else {
+                               return minutes;
+                       }
+               }
+
+               function updateTimezoneSelection() {
+                       var minuteDiff, localTime,
+                               type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
+                               val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
+
+                       if ( type === 'other' ) {
+                               // User specified time zone manually in <input>
+                               // Grab data from the textbox, parse it.
+                               minuteDiff = hoursToMinutes( val );
+                       } else {
+                               // Time zone not manually specified by user
+                               if ( type === 'guess' ) {
+                                       // Get browser timezone & fill it in
+                                       minuteDiff = -( new Date().getTimezoneOffset() );
+                                       if ( oouiEnabled ) {
+                                               timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+                                               timezoneWidget.dropdowninput.setValue( 'other' );
+                                       } else {
+                                               $tzTextbox.val( minutesToHours( minuteDiff ) );
+                                               $tzSelect.val( 'other' );
+                                       }
+                               } else {
+                                       // Grab data from the dropdown value
+                                       minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
+                               }
+                       }
+
+                       // Determine local time from server time and minutes difference, for display.
+                       localTime = servertime + minuteDiff;
+
+                       // Bring time within the [0,1440) range.
+                       localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
+
+                       $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
+               }
+
+               if ( oouiEnabled ) {
+                       if ( timezoneWidget ) {
+                               timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+                               timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
+                               updateTimezoneSelection();
+                       }
+               } else {
+                       if ( $tzSelect.length && $tzTextbox.length ) {
+                               $tzSelect.change( updateTimezoneSelection );
+                               $tzTextbox.blur( updateTimezoneSelection );
+                               updateTimezoneSelection();
+                       }
+               }
+
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.recentchanges.js b/resources/src/mediawiki.special.recentchanges.js
new file mode 100644 (file)
index 0000000..29c0fea
--- /dev/null
@@ -0,0 +1,38 @@
+/*!
+ * JavaScript for Special:RecentChanges
+ */
+( function ( mw, $ ) {
+       var rc, $checkboxes, $select;
+
+       /**
+        * @class mw.special.recentchanges
+        * @singleton
+        */
+       rc = {
+               /**
+                * Handler to disable/enable the namespace selector checkboxes when the
+                * special 'all' namespace is selected/unselected respectively.
+                */
+               updateCheckboxes: function () {
+                       // The option element for the 'all' namespace has an empty value
+                       var isAllNS = $select.val() === '';
+
+                       // Iterates over checkboxes and propagate the selected option
+                       $checkboxes.prop( 'disabled', isAllNS );
+               },
+
+               init: function () {
+                       $select = $( '#namespace' );
+                       $checkboxes = $( '#nsassociated, #nsinvert' );
+
+                       // Bind to change event, and trigger once to set the initial state of the checkboxes.
+                       rc.updateCheckboxes();
+                       $select.change( rc.updateCheckboxes );
+               }
+       };
+
+       $( rc.init );
+
+       module.exports = rc;
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.revisionDelete.js b/resources/src/mediawiki.special.revisionDelete.js
new file mode 100644 (file)
index 0000000..cad9db0
--- /dev/null
@@ -0,0 +1,29 @@
+/*!
+ * JavaScript for Special:RevisionDelete
+ */
+( function ( mw, $ ) {
+       var colonSeparator = mw.message( 'colon-separator' ).text(),
+               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+               $wpRevDeleteReasonList = $( '#wpRevDeleteReasonList' ),
+               $wpReason = $( '#wpReason' ),
+               filterFn = function ( input ) {
+                       // Should be built the same as in SpecialRevisionDelete::submit()
+                       var comment = $wpRevDeleteReasonList.val();
+                       if ( comment === 'other' ) {
+                               comment = input;
+                       } else if ( input !== '' ) {
+                               // Entry from drop down menu + additional comment
+                               comment += colonSeparator + input;
+                       }
+                       return comment;
+               };
+
+       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+       if ( summaryCodePointLimit ) {
+               $wpReason.codePointLimit( summaryCodePointLimit, filterFn );
+       } else if ( summaryByteLimit ) {
+               $wpReason.byteLimit( summaryByteLimit, filterFn );
+       }
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.search.commonsInterwikiWidget.js b/resources/src/mediawiki.special.search.commonsInterwikiWidget.js
new file mode 100644 (file)
index 0000000..648bf67
--- /dev/null
@@ -0,0 +1,78 @@
+( function ( mw, $ ) {
+
+       var api = new mw.Api(),
+               pageUrl = new mw.Uri(),
+               imagesText = new mw.Message( mw.messages, 'searchprofile-images' ),
+               moreResultsText = new mw.Message( mw.messages, 'search-interwiki-more-results' );
+
+       function itemTemplate( results ) {
+
+               var resultOutput = '', i, result, imageCaption, imageThumbnailSrc;
+
+               for ( i = 0; i < results.length; i++ ) {
+                       result = results[ i ];
+                       imageCaption = mw.html.element( 'span', { 'class': 'iw-result__mini-gallery__caption' }, result.title );
+                       imageThumbnailSrc = ( result.thumbnail ) ? result.thumbnail.source : '';
+                       resultOutput += '<div class="iw-result__mini-gallery">' +
+                                               /* escaping response content */
+                                               mw.html.element( 'a', {
+                                                       href: '/wiki/' + result.title,
+                                                       'class': 'iw-result__mini-gallery__image',
+                                                       style: 'background-image: url(' + imageThumbnailSrc + ');'
+                                               }, new mw.html.Raw( imageCaption ) ) +
+                                       '</div>';
+               }
+
+               return resultOutput;
+       }
+
+       function itemWrapperTemplate( pageQuery, itemTemplateOutput ) {
+
+               return '<li class="iw-resultset iw-resultset--image" data-iw-resultset-pos="0">' +
+                               '<div class="iw-result__header">' +
+                                       '<strong>' + imagesText.escaped() + '</strong>' +
+                               '</div>' +
+                               '<div class="iw-result__content">' +
+                               /* template output has been sanitized by mw.html.element */
+                               itemTemplateOutput +
+                               '</div>' +
+                               '<div class="iw-result__footer">' +
+                                       '<a href="/w/index.php?title=Special:Search&search=' + encodeURIComponent( pageQuery ) + '&fulltext=1&profile=images">' +
+                                               moreResultsText.escaped() +
+                                       '</a>' +
+                               '</div>' +
+                       '</li>';
+
+       }
+
+       api.get( {
+               action: 'query',
+               generator: 'search',
+               gsrsearch: pageUrl.query.search,
+               gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file,
+               gsrlimit: 3,
+               prop: 'pageimages',
+               pilimit: 3,
+               piprop: 'thumbnail',
+               pithumbsize: 300,
+               formatversion: 2
+       } ).done( function ( resp ) {
+               var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
+                       multimediaWidgetTemplate;
+
+               if ( !results ) {
+                       return;
+               }
+
+               results.sort( function ( a, b ) {
+                       return a.index - b.index;
+               } );
+
+               multimediaWidgetTemplate = itemWrapperTemplate( pageUrl.query.search, itemTemplate( results ) );
+               /* we really only need to wait for document ready for DOM manipulation */
+               $( function () {
+                       $( '.iw-results' ).append( multimediaWidgetTemplate );
+               } );
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.search.interwikiwidget.styles.less b/resources/src/mediawiki.special.search.interwikiwidget.styles.less
new file mode 100644 (file)
index 0000000..8ec2735
--- /dev/null
@@ -0,0 +1,121 @@
+/* interwiki search results */
+/*==========================*/
+
+@import 'mediawiki.ui/variables.less';
+@import 'mediawiki.mixins';
+
+.mw-searchresults-has-iw {
+
+       .iw-headline {
+               font-weight: bold;
+       }
+
+       .iw-results {
+               list-style: none;
+               margin: 0;
+       }
+
+       .iw-resultset {
+               .box-sizing(border-box);
+               padding: 0.5em;
+               vertical-align: top;
+               width: 100%;
+               float: left;
+               background-color: @colorGray15;
+               margin-bottom: 1em;
+               word-break: break-word;
+       }
+
+       .iw-result__title {
+               font-size: 108%; /* matching regular search title */
+       }
+
+       .iw-result:after,
+       .iw-result__content:after { /* clearfix */
+               visibility: hidden;
+               display: block;
+               font-size: 0;
+               content: ' ';
+               clear: both;
+               height: 0;
+       }
+
+       .iw-result__footer {
+               float: right;
+               font-size: 97%; /* matching main search result font-size */
+               margin-top: 0.5em;
+       }
+       .iw-result__footer a {
+               vertical-align: middle;
+               font-style: italic;
+       }
+
+       .oo-ui-icon-favicon {
+               padding-right: 1em;
+       }
+
+       /* image search result */
+       .iw-result__mini-gallery {
+               position: relative;
+               float: left;
+               width: 100%;
+               height: 200px;
+               .box-sizing(border-box);
+               padding: 0.25rem;
+       }
+
+       /* second and third images are small */
+       .iw-result__mini-gallery:nth-child( 2 ),
+       .iw-result__mini-gallery:nth-child( 3 ) { /* stylelint-disable-line indentation */
+               width: 50%;
+               height: 100px;
+       }
+
+       .iw-result__mini-gallery__image {
+               display: block;
+               position: relative;
+               width: 100%;
+               height: 100%;
+               background-size: 100% auto;
+               background-size: cover;
+               background-repeat: no-repeat;
+               background-position: center center;
+       }
+
+       /* image gallery text */
+       .iw-result__mini-gallery__image > .iw-result__mini-gallery__caption {
+               visibility: hidden;
+               position: absolute;
+               bottom: 0;
+               left: 0;
+               text-align: center;
+               color: #fff;
+               font-size: 0.8em;
+               padding: 0.5em;
+               background-color: rgba( 0, 0, 0, 0.5 );
+       }
+
+       .iw-result__mini-gallery__image:hover > .iw-result__mini-gallery__caption {
+               visibility: visible;
+       }
+
+       /* tablet and up */
+
+       @media only screen and ( min-width: @deviceWidthTablet ) {
+
+               #mw-interwiki-results {
+                       width: 30%;
+                       display: inline-block; /* used to align interwiki sidebar with the top of the main search results */
+                       margin-left: 8%; /* since inline-block causes whitespace issues, this is 8 instead of 10% */
+               }
+               .mw-search-createlink,
+               .mw-search-nonefound,
+               .mw-search-results,
+               .mw-search-interwiki-header {
+                       float: left;
+                       width: 60%;
+                       clear: left;
+                       max-width: 60%;
+               }
+       }
+}
diff --git a/resources/src/mediawiki.special.search.styles.css b/resources/src/mediawiki.special.search.styles.css
new file mode 100644 (file)
index 0000000..ea9b987
--- /dev/null
@@ -0,0 +1,166 @@
+/* Special:Search */
+
+/*
+ * Fixes sister projects box moving down the extract
+ * of the first result (bug #16886).
+ * It only happens when the window is small and
+ * This changes slightly the layout for big screens
+ * where there was space for the extracts and the
+ * sister projects and thus it showed like in any
+ * other browser.
+ *
+ * This will only affect IE 7 and lower
+ */
+.searchresult {
+       display: inline !ie;
+}
+.searchresults {
+       margin: 1em 0 1em 0.4em;
+}
+/* needs extra specificity to override `.mw-body p` selector */
+.mw-body .mw-search-nonefound {
+       margin: 0;
+}
+
+.searchdidyoumean em,
+.searchmatch {
+       font-weight: bold;
+}
+
+.mw-search-results {
+       margin: 0;
+       max-width: 38em;
+}
+
+.mw-search-visualclear {
+       clear: both;
+}
+.mw-search-results li {
+       padding-bottom: 1.2em;
+       list-style: none;
+       list-style-image: none;
+}
+.mw-search-results li a {
+       font-size: 108%;
+}
+.mw-search-result-data {
+       color: #008000;
+       font-size: 97%;
+}
+.mw-search-profile-tabs {
+       background-color: #f8f9fa;
+       margin-top: 1em;
+       border: 1px solid #c8ccd1;
+       border-radius: 2px;
+}
+.search-types {
+       float: left;
+       padding-left: 0.25em;
+}
+.search-types ul {
+       margin: 0;
+       padding: 0;
+       list-style: none;
+}
+.search-types li {
+       float: left;
+       margin: 0;
+       padding: 0;
+}
+.search-types a {
+       display: block;
+       padding: 0.5em;
+}
+.search-types .current a {
+       color: #222;
+       cursor: default;
+}
+.search-types .current a:hover {
+       text-decoration: none;
+}
+.results-info {
+       float: right;
+       padding: 0.5em;
+       padding-right: 0.75em;
+       color: #54595d;
+       font-size: 95%;
+}
+#mw-search-top-table div.oo-ui-actionFieldLayout {
+       float: left;
+       width: 100%;
+}
+
+/* Advanced options menu */
+/*==========================*/
+
+#mw-searchoptions {
+       /* Support: Firefox, needs `clear: both` on `fieldset` when zoom level > 100%, see T176499 */
+       clear: both;
+       padding: 0.5em 0.75em 0.75em 0.75em;
+       background-color: #f8f9fa;
+       margin: -1px 0 0;
+       border: 1px solid #c8ccd1;
+       border-radius: 0 0 2px 2px;
+}
+#mw-searchoptions legend {
+       display: none;
+}
+#mw-searchoptions h4 {
+       padding: 0;
+       margin: 0;
+       float: left;
+}
+#mw-searchoptions table {
+       float: left;
+       margin-right: 3em;
+       border-collapse: collapse;
+}
+#mw-searchoptions table td {
+       padding: 0 1em 0 0;
+       white-space: nowrap;
+}
+#mw-searchoptions .divider {
+       clear: both;
+       border-bottom: 1px solid #eaecf0;
+       padding-top: 0.5em;
+       margin-bottom: 0.5em;
+}
+#mw-search-menu {
+       padding-left: 6em;
+       font-size: 85%;
+}
+
+#mw-search-interwiki {
+       float: right;
+       width: 18em;
+       border: 1px solid #a2a9b1;
+       margin-top: 2ex;
+}
+
+.searchalttitle,
+#mw-search-interwiki li {
+       font-size: 95%;
+}
+.mw-search-interwiki-more {
+       float: right;
+       font-size: 90%;
+}
+#mw-search-interwiki-caption {
+       text-align: center;
+       font-weight: bold;
+       font-size: 95%;
+}
+.mw-search-interwiki-project {
+       font-size: 97%;
+       text-align: left;
+       padding: 0.15em 0.15em 0.2em 0.2em;
+       background-color: #eaecf0;
+       border-top: 1px solid #c8ccd1;
+}
+
+.searchdidyoumean {
+       font-size: 127%;
+       margin-top: 0.8em;
+       /* Note that this color won't affect the link, as desired. */
+       color: #d33;
+}
diff --git a/resources/src/mediawiki.special.search/search.css b/resources/src/mediawiki.special.search/search.css
new file mode 100644 (file)
index 0000000..aad784e
--- /dev/null
@@ -0,0 +1,9 @@
+#mw-search-togglebox {
+       float: right;
+}
+#mw-search-togglebox label {
+       margin-right: 0.25em;
+}
+#mw-search-togglebox input {
+       margin-left: 0.25em;
+}
diff --git a/resources/src/mediawiki.special.search/search.js b/resources/src/mediawiki.special.search/search.js
new file mode 100644 (file)
index 0000000..e809f2e
--- /dev/null
@@ -0,0 +1,60 @@
+/*!
+ * JavaScript for Special:Search
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
+
+               // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
+               if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
+                       $( 'input[autofocus]' ).eq( 0 ).focus();
+               }
+
+               // Create check all/none button
+               $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
+               $( '#mw-search-togglebox' ).append(
+                       $( '<label>' )
+                               .text( mw.msg( 'powersearch-togglelabel' ) )
+               ).append(
+                       $( '<input>' ).attr( 'type', 'button' )
+                               .attr( 'id', 'mw-search-toggleall' )
+                               .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
+                               .click( function () {
+                                       $checkboxes.prop( 'checked', true );
+                               } )
+               ).append(
+                       $( '<input>' ).attr( 'type', 'button' )
+                               .attr( 'id', 'mw-search-togglenone' )
+                               .prop( 'value', mw.msg( 'powersearch-togglenone' ) )
+                               .click( function () {
+                                       $checkboxes.prop( 'checked', false );
+                               } )
+               );
+
+               // Change the header search links to what user entered
+               $headerLinks = $( '.search-types a' );
+               searchWidget = OO.ui.infuse( 'searchText' );
+               updateHeaderLinks = function ( value ) {
+                       $headerLinks.each( function () {
+                               var parts = $( this ).attr( 'href' ).split( 'search=' ),
+                                       lastpart = '',
+                                       prefix = 'search=';
+                               if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
+                                       lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
+                               } else {
+                                       prefix = '&search=';
+                               }
+                               this.href = parts[ 0 ] + prefix + encodeURIComponent( value ) + lastpart;
+                       } );
+               };
+               searchWidget.on( 'change', updateHeaderLinks );
+               updateHeaderLinks( searchWidget.getValue() );
+
+               // When saving settings, use the proper request method (POST instead of GET).
+               $( '#mw-search-powersearch-remember' ).change( function () {
+                       this.form.method = this.checked ? 'post' : 'get';
+               } ).trigger( 'change' );
+
+       } );
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.undelete.js b/resources/src/mediawiki.special.undelete.js
new file mode 100644 (file)
index 0000000..e3cf598
--- /dev/null
@@ -0,0 +1,23 @@
+/*!
+ * JavaScript for Special:Undelete
+ */
+( function ( mw, $ ) {
+       $( function () {
+               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+                       wpComment = OO.ui.infuse( $( '#wpComment' ).closest( '.oo-ui-widget' ) );
+
+               $( '#mw-undelete-invert' ).click( function () {
+                       $( '.mw-undelete-revlist input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
+                               return !val;
+                       } );
+               } );
+
+               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+               if ( summaryCodePointLimit ) {
+                       mw.widgets.visibleCodePointLimit( wpComment, summaryCodePointLimit );
+               } else if ( summaryByteLimit ) {
+                       mw.widgets.visibleByteLimit( wpComment, summaryByteLimit );
+               }
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css b/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.css
new file mode 100644 (file)
index 0000000..69fec08
--- /dev/null
@@ -0,0 +1,7 @@
+.mw-watched-item {
+       text-decoration: line-through;
+}
+
+.mw-watch-link-disabled {
+       pointer-events: none;
+}
diff --git a/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js b/resources/src/mediawiki.special.unwatchedPages/unwatchedPages.js
new file mode 100644 (file)
index 0000000..0886f8c
--- /dev/null
@@ -0,0 +1,49 @@
+/*!
+ * JavaScript for Special:UnwatchedPages
+ */
+( function ( mw, $ ) {
+       $( function () {
+               $( 'a.mw-watch-link' ).click( function ( e ) {
+                       var promise,
+                               api = new mw.Api(),
+                               $link = $( this ),
+                               $subjectLink = $link.closest( 'li' ).children( 'a' ).eq( 0 ),
+                               title = mw.util.getParamValue( 'title', $link.attr( 'href' ) );
+                       // nice format
+                       title = mw.Title.newFromText( title ).toText();
+                       $link.addClass( 'mw-watch-link-disabled' );
+
+                       // Preload the notification module for mw.notify
+                       mw.loader.load( 'mediawiki.notification' );
+
+                       // Use the class to determine whether to watch or unwatch
+                       if ( !$subjectLink.hasClass( 'mw-watched-item' ) ) {
+                               $link.text( mw.msg( 'watching' ) );
+                               promise = api.watch( title ).done( function () {
+                                       $subjectLink.addClass( 'mw-watched-item' );
+                                       $link.text( mw.msg( 'unwatch' ) );
+                                       mw.notify( mw.msg( 'addedwatchtext-short', title ) );
+                               } ).fail( function () {
+                                       $link.text( mw.msg( 'watch' ) );
+                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
+                               } );
+                       } else {
+                               $link.text( mw.msg( 'unwatching' ) );
+                               promise = api.unwatch( title ).done( function () {
+                                       $subjectLink.removeClass( 'mw-watched-item' );
+                                       $link.text( mw.msg( 'watch' ) );
+                                       mw.notify( mw.msg( 'removedwatchtext-short', title ) );
+                               } ).fail( function () {
+                                       $link.text( mw.msg( 'unwatch' ) );
+                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
+                               } );
+                       }
+
+                       promise.always( function () {
+                               $link.removeClass( 'mw-watch-link-disabled' );
+                       } );
+
+                       e.preventDefault();
+               } );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.upload.styles.css b/resources/src/mediawiki.special.upload.styles.css
new file mode 100644 (file)
index 0000000..626a7e8
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for Special:Upload
+ */
+.mw-destfile-warning {
+       border: 1px solid #fde29b;
+       padding: 0.5em 1em;
+       margin-bottom: 1em;
+       color: #705000;
+       background-color: #fdf1d1;
+}
+
+p.mw-upload-editlicenses {
+       font-size: 90%;
+       text-align: right;
+}
diff --git a/resources/src/mediawiki.special.upload/templates/thumbnail.html b/resources/src/mediawiki.special.upload/templates/thumbnail.html
new file mode 100644 (file)
index 0000000..bf0e701
--- /dev/null
@@ -0,0 +1,8 @@
+<div id="mw-upload-thumbnail" class="thumb tright">
+       <div class="thumbinner">
+               <div class="thumbcaption">
+                       <div class="filename"></div>
+                       <div class="fileinfo"></div>
+               </div>
+       </div>
+</div>
diff --git a/resources/src/mediawiki.special.upload/upload.js b/resources/src/mediawiki.special.upload/upload.js
new file mode 100644 (file)
index 0000000..144659a
--- /dev/null
@@ -0,0 +1,654 @@
+/**
+ * JavaScript for Special:Upload
+ *
+ * @private
+ * @class mw.special.upload
+ * @singleton
+ */
+
+/* global Uint8Array */
+
+( function ( mw, $ ) {
+       var uploadWarning, uploadTemplatePreview,
+               ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
+               $license = $( '#wpLicense' );
+
+       window.wgUploadWarningObj = uploadWarning = {
+               responseCache: { '': '&nbsp;' },
+               nameToCheck: '',
+               typing: false,
+               delay: 500, // ms
+               timeoutID: false,
+
+               keypress: function () {
+                       if ( !ajaxUploadDestCheck ) {
+                               return;
+                       }
+
+                       // Find file to upload
+                       if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
+                               return;
+                       }
+
+                       this.nameToCheck = $( '#wpDestFile' ).val();
+
+                       // Clear timer
+                       if ( this.timeoutID ) {
+                               clearTimeout( this.timeoutID );
+                       }
+                       // Check response cache
+                       if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
+                               this.setWarning( this.responseCache[ this.nameToCheck ] );
+                               return;
+                       }
+
+                       this.timeoutID = setTimeout( function () {
+                               uploadWarning.timeout();
+                       }, this.delay );
+               },
+
+               checkNow: function ( fname ) {
+                       if ( !ajaxUploadDestCheck ) {
+                               return;
+                       }
+                       if ( this.timeoutID ) {
+                               clearTimeout( this.timeoutID );
+                       }
+                       this.nameToCheck = fname;
+                       this.timeout();
+               },
+
+               timeout: function () {
+                       var $spinnerDestCheck, title;
+                       if ( !ajaxUploadDestCheck || this.nameToCheck.trim() === '' ) {
+                               return;
+                       }
+                       $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
+                       title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
+
+                       ( new mw.Api() ).get( {
+                               formatversion: 2,
+                               action: 'query',
+                               // If title is empty, user input is invalid, the API call will produce details about why
+                               titles: [ title ? title.getPrefixedText() : this.nameToCheck ],
+                               prop: 'imageinfo',
+                               iiprop: 'uploadwarning',
+                               errorformat: 'html',
+                               errorlang: mw.config.get( 'wgUserLanguage' )
+                       } ).done( function ( result ) {
+                               var
+                                       resultOut = '',
+                                       page = result.query.pages[ 0 ];
+                               if ( page.imageinfo ) {
+                                       resultOut = page.imageinfo[ 0 ].html;
+                               } else if ( page.invalidreason ) {
+                                       resultOut = page.invalidreason.html;
+                               }
+                               uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
+                       } ).always( function () {
+                               $spinnerDestCheck.remove();
+                       } );
+               },
+
+               processResult: function ( result, fileName ) {
+                       this.setWarning( result );
+                       this.responseCache[ fileName ] = result;
+               },
+
+               setWarning: function ( warning ) {
+                       var $warningBox = $( '#wpDestFile-warning' ),
+                               $warning = $( $.parseHTML( warning ) );
+                       mw.hook( 'wikipage.content' ).fire( $warning );
+                       $warningBox.empty().append( $warning );
+
+                       // Set a value in the form indicating that the warning is acknowledged and
+                       // doesn't need to be redisplayed post-upload
+                       if ( !warning ) {
+                               $( '#wpDestFileWarningAck' ).val( '' );
+                               $warningBox.removeAttr( 'class' );
+                       } else {
+                               $( '#wpDestFileWarningAck' ).val( '1' );
+                               $warningBox.attr( 'class', 'mw-destfile-warning' );
+                       }
+
+               }
+       };
+
+       window.wgUploadTemplatePreviewObj = uploadTemplatePreview = {
+
+               responseCache: { '': '' },
+
+               /**
+                * @param {jQuery} $element The element whose .val() will be previewed
+                * @param {jQuery} $previewContainer The container to display the preview in
+                */
+               getPreview: function ( $element, $previewContainer ) {
+                       var template = $element.val(),
+                               $spinner;
+
+                       if ( this.responseCache.hasOwnProperty( template ) ) {
+                               this.showPreview( this.responseCache[ template ], $previewContainer );
+                               return;
+                       }
+
+                       $spinner = $.createSpinner().insertAfter( $element );
+
+                       ( new mw.Api() ).parse( '{{' + template + '}}', {
+                               title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
+                               prop: 'text',
+                               pst: true,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       } ).done( function ( result ) {
+                               uploadTemplatePreview.processResult( result, template, $previewContainer );
+                       } ).always( function () {
+                               $spinner.remove();
+                       } );
+               },
+
+               processResult: function ( result, template, $previewContainer ) {
+                       this.responseCache[ template ] = result;
+                       this.showPreview( this.responseCache[ template ], $previewContainer );
+               },
+
+               showPreview: function ( preview, $previewContainer ) {
+                       $previewContainer.html( preview );
+               }
+
+       };
+
+       $( function () {
+               // AJAX wpDestFile warnings
+               if ( ajaxUploadDestCheck ) {
+                       // Insert an event handler that fetches upload warnings when wpDestFile
+                       // has been changed
+                       $( '#wpDestFile' ).change( function () {
+                               uploadWarning.checkNow( $( this ).val() );
+                       } );
+                       // Insert a row where the warnings will be displayed just below the
+                       // wpDestFile row
+                       $( '#mw-htmlform-description tbody' ).append(
+                               $( '<tr>' ).append(
+                                       $( '<td>' )
+                                               .attr( 'id', 'wpDestFile-warning' )
+                                               .attr( 'colspan', 2 )
+                               )
+                       );
+               }
+
+               if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
+                       // License selector check
+                       $license.change( function () {
+                               // We might show a preview
+                               uploadTemplatePreview.getPreview( $license, $( '#mw-license-preview' ) );
+                       } );
+
+                       // License selector table row
+                       $license.closest( 'tr' ).after(
+                               $( '<tr>' ).append(
+                                       $( '<td>' ),
+                                       $( '<td>' ).attr( 'id', 'mw-license-preview' )
+                               )
+                       );
+               }
+
+               // fillDestFile setup
+               mw.config.get( 'wgUploadSourceIds' ).forEach( function ( sourceId ) {
+                       $( '#' + sourceId ).change( function () {
+                               var path, slash, backslash, fname;
+                               if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
+                                       return;
+                               }
+                               // Remove any previously flagged errors
+                               $( '#mw-upload-permitted' ).attr( 'class', '' );
+                               $( '#mw-upload-prohibited' ).attr( 'class', '' );
+
+                               path = $( this ).val();
+                               // Find trailing part
+                               slash = path.lastIndexOf( '/' );
+                               backslash = path.lastIndexOf( '\\' );
+                               if ( slash === -1 && backslash === -1 ) {
+                                       fname = path;
+                               } else if ( slash > backslash ) {
+                                       fname = path.slice( slash + 1 );
+                               } else {
+                                       fname = path.slice( backslash + 1 );
+                               }
+
+                               // Clear the filename if it does not have a valid extension.
+                               // URLs are less likely to have a useful extension, so don't include them in the
+                               // extension check.
+                               if (
+                                       mw.config.get( 'wgCheckFileExtensions' ) &&
+                                       mw.config.get( 'wgStrictFileExtensions' ) &&
+                                       Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
+                                       $( this ).attr( 'id' ) !== 'wpUploadFileURL'
+                               ) {
+                                       if (
+                                               fname.lastIndexOf( '.' ) === -1 ||
+                                               mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
+                                                       return element.toLowerCase();
+                                               } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
+                                       ) {
+                                               // Not a valid extension
+                                               // Clear the upload and set mw-upload-permitted to error
+                                               $( this ).val( '' );
+                                               $( '#mw-upload-permitted' ).attr( 'class', 'error' );
+                                               $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
+                                               // Clear wpDestFile as well
+                                               $( '#wpDestFile' ).val( '' );
+
+                                               return false;
+                                       }
+                               }
+
+                               // Replace spaces by underscores
+                               fname = fname.replace( / /g, '_' );
+                               // Capitalise first letter if needed
+                               if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
+                                       fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
+                               }
+
+                               // Output result
+                               if ( $( '#wpDestFile' ).length ) {
+                                       // Call decodeURIComponent function to remove possible URL-encoded characters
+                                       // from the file name (T32390). Especially likely with upload-form-url.
+                                       // decodeURIComponent can throw an exception if input is invalid utf-8
+                                       try {
+                                               $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
+                                       } catch ( err ) {
+                                               $( '#wpDestFile' ).val( fname );
+                                       }
+                                       uploadWarning.checkNow( fname );
+                               }
+                       } );
+               } );
+       } );
+
+       // Add a preview to the upload form
+       $( function () {
+               /**
+                * Is the FileAPI available with sufficient functionality?
+                *
+                * @return {boolean}
+                */
+               function hasFileAPI() {
+                       return window.FileReader !== undefined;
+               }
+
+               /**
+                * Check if this is a recognizable image type...
+                * Also excludes files over 10M to avoid going insane on memory usage.
+                *
+                * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
+                *
+                * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
+                *
+                * @param {File} file
+                * @return {boolean}
+                */
+               function fileIsPreviewable( file ) {
+                       var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
+                               tooHuge = 10 * 1024 * 1024;
+                       return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
+               }
+
+               /**
+                * Format a file size attractively.
+                *
+                * TODO: Match numeric formatting
+                *
+                * @param {number} s
+                * @return {string}
+                */
+               function prettySize( s ) {
+                       var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
+                       while ( s >= 1024 && sizeMsgs.length > 1 ) {
+                               s /= 1024;
+                               sizeMsgs = sizeMsgs.slice( 1 );
+                       }
+                       return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
+               }
+
+               /**
+                * Start loading a file into memory; when complete, pass it as a
+                * data URL to the callback function. If the callbackBinary is set it will
+                * first be read as binary and afterwards as data URL. Useful if you want
+                * to do preprocessing on the binary data first.
+                *
+                * @param {File} file
+                * @param {Function} callback
+                * @param {Function} callbackBinary
+                */
+               function fetchPreview( file, callback, callbackBinary ) {
+                       var reader = new FileReader();
+                       if ( callbackBinary && 'readAsBinaryString' in reader ) {
+                               // To fetch JPEG metadata we need a binary string; start there.
+                               // TODO
+                               reader.onload = function () {
+                                       callbackBinary( reader.result );
+
+                                       // Now run back through the regular code path.
+                                       fetchPreview( file, callback );
+                               };
+                               reader.readAsBinaryString( file );
+                       } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
+                               // readAsArrayBuffer replaces readAsBinaryString
+                               // However, our JPEG metadata library wants a string.
+                               // So, this is going to be an ugly conversion.
+                               reader.onload = function () {
+                                       var i,
+                                               buffer = new Uint8Array( reader.result ),
+                                               string = '';
+                                       for ( i = 0; i < buffer.byteLength; i++ ) {
+                                               string += String.fromCharCode( buffer[ i ] );
+                                       }
+                                       callbackBinary( string );
+
+                                       // Now run back through the regular code path.
+                                       fetchPreview( file, callback );
+                               };
+                               reader.readAsArrayBuffer( file );
+                       } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
+                               // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL>
+                               // WebKit has it in a namespace for now but that's ok. ;)
+                               //
+                               // Lifetime of this URL is until document close, which is fine
+                               // for Special:Upload -- if this code gets used on longer-running
+                               // pages, add a revokeObjectURL() when it's no longer needed.
+                               //
+                               // Prefer this over readAsDataURL for Firefox 7 due to bug reading
+                               // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
+                               callback( window.URL.createObjectURL( file ) );
+                       } else {
+                               // This ends up decoding the file to base-64 and back again, which
+                               // feels horribly inefficient.
+                               reader.onload = function () {
+                                       callback( reader.result );
+                               };
+                               reader.readAsDataURL( file );
+                       }
+               }
+
+               /**
+                * Clear the file upload preview area.
+                */
+               function clearPreview() {
+                       $( '#mw-upload-thumbnail' ).remove();
+               }
+
+               /**
+                * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
+                * in browsers supporting HTML5 FileAPI.
+                *
+                * As of this writing, known good:
+                *
+                * - Firefox 3.6+
+                * - Chrome 7.something
+                *
+                * TODO: Check file size limits and warn of likely failures
+                *
+                * @param {File} file
+                */
+               function showPreview( file ) {
+                       var $canvas,
+                               ctx,
+                               meta,
+                               previewSize = 180,
+                               $spinner = $.createSpinner( { size: 'small', type: 'block' } )
+                                       .css( { width: previewSize, height: previewSize } ),
+                               thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
+
+                       thumb
+                               .find( '.filename' ).text( file.name ).end()
+                               .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
+                               .find( '.thumbinner' ).prepend( $spinner ).end();
+
+                       $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
+                       ctx = $canvas[ 0 ].getContext( '2d' );
+                       $( '#mw-htmlform-source' ).parent().prepend( thumb );
+
+                       fetchPreview( file, function ( dataURL ) {
+                               var img = new Image(),
+                                       rotation = 0;
+
+                               if ( meta && meta.tiff && meta.tiff.Orientation ) {
+                                       rotation = ( 360 - ( function () {
+                                               // See BitmapHandler class in PHP
+                                               switch ( meta.tiff.Orientation.value ) {
+                                                       case 8:
+                                                               return 90;
+                                                       case 3:
+                                                               return 180;
+                                                       case 6:
+                                                               return 270;
+                                                       default:
+                                                               return 0;
+                                               }
+                                       }() ) ) % 360;
+                               }
+
+                               img.onload = function () {
+                                       var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
+
+                                       // Fit the image within the previewSizexpreviewSize box
+                                       if ( img.width > img.height ) {
+                                               width = previewSize;
+                                               height = img.height / img.width * previewSize;
+                                       } else {
+                                               height = previewSize;
+                                               width = img.width / img.height * previewSize;
+                                       }
+                                       // Determine the offset required to center the image
+                                       dx = ( 180 - width ) / 2;
+                                       dy = ( 180 - height ) / 2;
+                                       switch ( rotation ) {
+                                               // If a rotation is applied, the direction of the axis
+                                               // changes as well. You can derive the values below by
+                                               // drawing on paper an axis system, rotate it and see
+                                               // where the positive axis direction is
+                                               case 0:
+                                                       x = dx;
+                                                       y = dy;
+                                                       logicalWidth = img.width;
+                                                       logicalHeight = img.height;
+                                                       break;
+                                               case 90:
+
+                                                       x = dx;
+                                                       y = dy - previewSize;
+                                                       logicalWidth = img.height;
+                                                       logicalHeight = img.width;
+                                                       break;
+                                               case 180:
+                                                       x = dx - previewSize;
+                                                       y = dy - previewSize;
+                                                       logicalWidth = img.width;
+                                                       logicalHeight = img.height;
+                                                       break;
+                                               case 270:
+                                                       x = dx - previewSize;
+                                                       y = dy;
+                                                       logicalWidth = img.height;
+                                                       logicalHeight = img.width;
+                                                       break;
+                                       }
+
+                                       ctx.clearRect( 0, 0, 180, 180 );
+                                       ctx.rotate( rotation / 180 * Math.PI );
+                                       ctx.drawImage( img, x, y, width, height );
+                                       $spinner.replaceWith( $canvas );
+
+                                       // Image size
+                                       info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
+                                               ', ' + prettySize( file.size );
+
+                                       $( '#mw-upload-thumbnail .fileinfo' ).text( info );
+                               };
+                               img.onerror = function () {
+                                       // Can happen for example for invalid SVG files
+                                       clearPreview();
+                               };
+                               img.src = dataURL;
+                       }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
+                               var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
+                               try {
+                                       meta = jpegmeta( data, file.fileName );
+                                       // eslint-disable-next-line no-underscore-dangle, camelcase
+                                       meta._binary_data = null;
+                               } catch ( e ) {
+                                       meta = null;
+                               }
+                       } : null );
+               }
+
+               /**
+                * Check if the file does not exceed the maximum size
+                *
+                * @param {File} file
+                * @return {boolean}
+                */
+               function checkMaxUploadSize( file ) {
+                       var maxSize, $error;
+
+                       function getMaxUploadSize( type ) {
+                               var sizes = mw.config.get( 'wgMaxUploadSize' );
+
+                               if ( sizes[ type ] !== undefined ) {
+                                       return sizes[ type ];
+                               }
+                               return sizes[ '*' ];
+                       }
+
+                       $( '.mw-upload-source-error' ).remove();
+
+                       maxSize = getMaxUploadSize( 'file' );
+                       if ( file.size > maxSize ) {
+                               $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
+                                       mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
+
+                               $( '#wpUploadFile' ).after( $error );
+
+                               return false;
+                       }
+
+                       return true;
+               }
+
+               /* Initialization */
+               if ( hasFileAPI() ) {
+                       // Update thumbnail when the file selection control is updated.
+                       $( '#wpUploadFile' ).change( function () {
+                               var file;
+                               clearPreview();
+                               if ( this.files && this.files.length ) {
+                                       // Note: would need to be updated to handle multiple files.
+                                       file = this.files[ 0 ];
+
+                                       if ( !checkMaxUploadSize( file ) ) {
+                                               return;
+                                       }
+
+                                       if ( fileIsPreviewable( file ) ) {
+                                               showPreview( file );
+                                       }
+                               }
+                       } );
+               }
+       } );
+
+       // Disable all upload source fields except the selected one
+       $( function () {
+               var $rows = $( '.mw-htmlform-field-UploadSourceField' );
+
+               $rows.on( 'change', 'input[type="radio"]', function ( e ) {
+                       var currentRow = e.delegateTarget;
+
+                       if ( !this.checked ) {
+                               return;
+                       }
+
+                       $( '.mw-upload-source-error' ).remove();
+
+                       // Enable selected upload method
+                       $( currentRow ).find( 'input' ).prop( 'disabled', false );
+
+                       // Disable inputs of other upload methods
+                       // (except for the radio button to re-enable it)
+                       $rows
+                               .not( currentRow )
+                               .find( 'input[type!="radio"]' )
+                               .prop( 'disabled', true );
+               } );
+
+               // Set initial state
+               if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
+                       $( '#wpUploadFileURL' ).prop( 'disabled', true );
+               }
+       } );
+
+       $( function () {
+               // Prevent losing work
+               var allowCloseWindow,
+                       $uploadForm = $( '#mw-upload-form' );
+
+               if ( !mw.user.options.get( 'useeditwarning' ) ) {
+                       // If the user doesn't want edit warnings, don't set things up.
+                       return;
+               }
+
+               $uploadForm.data( 'origtext', $uploadForm.serialize() );
+
+               allowCloseWindow = mw.confirmCloseWindow( {
+                       test: function () {
+                               return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
+                                       $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
+                       },
+
+                       message: mw.msg( 'editwarning-warning' ),
+                       namespace: 'uploadwarning'
+               } );
+
+               $uploadForm.submit( function () {
+                       allowCloseWindow.release();
+               } );
+       } );
+
+       // Add tabindex to mw-editTools
+       $( function () {
+               // Function to change tabindex for all links within mw-editTools
+               function setEditTabindex( $val ) {
+                       $( '.mw-editTools' ).find( 'a' ).each( function () {
+                               $( this ).attr( 'tabindex', $val );
+                       } );
+               }
+
+               // Change tabindex to 0 if user pressed spaced or enter while focused
+               $( '.mw-editTools' ).on( 'keypress', function ( e ) {
+                       // Don't continue if pressed key was not enter or spacebar
+                       if ( e.which !== 13 && e.which !== 32 ) {
+                               return;
+                       }
+
+                       // Change tabindex only when main div has focus
+                       if ( $( this ).is( ':focus' ) ) {
+                               $( this ).find( 'a' ).first().focus();
+                               setEditTabindex( '0' );
+                       }
+               } );
+
+               // Reset tabindex for elements when user focused out mw-editTools
+               $( '.mw-editTools' ).on( 'focusout', function ( e ) {
+                       // Don't continue if relatedTarget is within mw-editTools
+                       if ( e.relatedTarget !== null && $( e.relatedTarget ).closest( '.mw-editTools' ).length > 0 ) {
+                               return;
+                       }
+
+                       // Reset tabindex back to -1
+                       setEditTabindex( '-1' );
+               } );
+
+               // Set initial tabindex for mw-editTools to 0 and to -1 for all links
+               $( '.mw-editTools' ).attr( 'tabindex', '0' );
+               setEditTabindex( '-1' );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png b/resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png
new file mode 100644 (file)
index 0000000..03f0eec
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.common.styles/images/icon-lock.png differ
diff --git a/resources/src/mediawiki.special.userlogin.common.styles/userlogin.css b/resources/src/mediawiki.special.userlogin.common.styles/userlogin.css
new file mode 100644 (file)
index 0000000..2366249
--- /dev/null
@@ -0,0 +1,75 @@
+/* User login and signup forms */
+.mw-ui-vform .mw-form-related-link-container {
+       margin-bottom: 0.5em;
+       text-align: center;
+}
+
+.mw-ui-vform .mw-secure {
+       /* @embed */
+       background: url( images/icon-lock.png ) no-repeat left center;
+       margin: 0 0 0 1px;
+       padding: 0 0 0 11px;
+}
+
+/*
+ * When inside the VForm style, disable the border that Vector and other skins
+ * put on the div surrounding the login/create account form.
+ * Also disable the margin and padding that Vector puts around the form.
+ */
+.mw-ui-container #userloginForm,
+.mw-ui-container #userlogin {
+       border: 0;
+       margin: 0;
+       padding: 0;
+}
+
+/* Reposition and resize language links, which appear on a per-wiki basis */
+.mw-ui-container #languagelinks {
+       margin-bottom: 2em;
+       font-size: 0.8em;
+}
+
+/* Put some space under template's header, which may contain CAPTCHA HTML. */
+section.mw-form-header {
+       margin-bottom: 10px;
+}
+
+/* shuffled CAPTCHA */
+#wpCaptchaWord {
+       margin-top: 6px;
+}
+
+.fancycaptcha-captcha-container {
+       background-color: #f8f9fa;
+       margin-bottom: 15px;
+       border: 1px solid #c8ccd1;
+       border-radius: 2px;
+       padding: 8px;
+       text-align: center;
+}
+
+.mw-createacct-captcha-assisted {
+       display: block;
+       margin-top: 0.5em;
+}
+
+/* Put a border around the fancycaptcha-image-container. */
+.fancycaptcha-captcha-and-reload {
+       border: 1px solid #c8ccd1;
+       border-radius: 2px 2px 0 0;
+       /* Other display formats end up too wide */
+       display: table-cell;
+       width: 270px;
+       background-color: #fff;
+}
+
+.fancycaptcha-captcha-container .mw-ui-input {
+       margin-top: -1px;
+       border-color: #c8ccd1;
+       border-radius: 0 0 2px 2px;
+}
+
+/* Make the fancycaptcha-image-container full-width within its parent. */
+.fancycaptcha-image-container {
+       width: 100%;
+}
diff --git a/resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png b/resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png
new file mode 100644 (file)
index 0000000..cba3caf
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.login.styles/images/glyph-people-large.png differ
diff --git a/resources/src/mediawiki.special.userlogin.login.styles/login.css b/resources/src/mediawiki.special.userlogin.login.styles/login.css
new file mode 100644 (file)
index 0000000..fe013bc
--- /dev/null
@@ -0,0 +1,29 @@
+/* The login form invites users to create an account */
+#mw-createaccount-cta {
+       width: 20em;
+       /* @embed */
+       background: url( images/glyph-people-large.png ) no-repeat 50%;
+       margin: 0 auto;
+       padding-top: 7.8em;
+       font-weight: bold;
+}
+
+/* Login Button, following 'ButtonWidget (progressive)' from OOUI */
+#mw-createaccount-join {
+       background-color: #f8f9fa;
+       color: #36c;
+}
+#mw-createaccount-join:hover {
+       background-color: #fff;
+       border-color: #859ecc;
+       box-shadow: none;
+}
+#mw-createaccount-join:active {
+       background-color: #eff3fa;
+       color: #2a4b8d;
+       border-color: #2a4b8d;
+}
+#mw-createaccount-join:focus {
+       border-color: #36c;
+       box-shadow: inset 0 0 0 1px #36c;
+}
diff --git a/resources/src/mediawiki.special.userlogin.signup.js b/resources/src/mediawiki.special.userlogin.signup.js
new file mode 100644 (file)
index 0000000..8a61afb
--- /dev/null
@@ -0,0 +1,122 @@
+/*!
+ * JavaScript for signup form.
+ */
+( function ( mw, $ ) {
+       // When sending password by email, hide the password input fields.
+       $( function () {
+               // Always required if checked, otherwise it depends, so we use the original
+               var $emailLabel = $( 'label[for="wpEmail"]' ),
+                       originalText = $emailLabel.text(),
+                       requiredText = mw.message( 'createacct-emailrequired' ).text(),
+                       $createByMailCheckbox = $( '#wpCreateaccountMail' ),
+                       $beforePwds = $( '.mw-row-password:first' ).prev(),
+                       $pwds;
+
+               function updateForCheckbox() {
+                       var checked = $createByMailCheckbox.prop( 'checked' );
+                       if ( checked ) {
+                               $pwds = $( '.mw-row-password' ).detach();
+                               $emailLabel.text( requiredText );
+                       } else {
+                               if ( $pwds ) {
+                                       $beforePwds.after( $pwds );
+                                       $pwds = null;
+                               }
+                               $emailLabel.text( originalText );
+                       }
+               }
+
+               $createByMailCheckbox.on( 'change', updateForCheckbox );
+               updateForCheckbox();
+       } );
+
+       // Check if the username is invalid or already taken
+       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
+               var $usernameInput = $root.find( '#wpName2' ),
+                       $passwordInput = $root.find( '#wpPassword2' ),
+                       $emailInput = $root.find( '#wpEmail' ),
+                       $realNameInput = $root.find( '#wpRealName' ),
+                       api = new mw.Api(),
+                       usernameChecker, passwordChecker;
+
+               function checkUsername( username ) {
+                       // We could just use .then() if we didn't have to pass on .abort()…
+                       var d, apiPromise;
+
+                       d = $.Deferred();
+                       apiPromise = api.get( {
+                               action: 'query',
+                               list: 'users',
+                               ususers: username,
+                               usprop: 'cancreate',
+                               formatversion: 2,
+                               errorformat: 'html',
+                               errorsuselocal: true,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       } )
+                               .done( function ( resp ) {
+                                       var userinfo = resp.query.users[ 0 ];
+
+                                       if ( resp.query.users.length !== 1 || userinfo.invalid ) {
+                                               d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
+                                       } else if ( userinfo.userid !== undefined ) {
+                                               d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
+                                       } else if ( !userinfo.cancreate ) {
+                                               d.resolve( {
+                                                       valid: false,
+                                                       messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
+                                                               return m.html;
+                                                       } ) : []
+                                               } );
+                                       } else {
+                                               d.resolve( { valid: true, messages: [] } );
+                                       }
+                               } )
+                               .fail( d.reject );
+
+                       return d.promise( { abort: apiPromise.abort } );
+               }
+
+               function checkPassword() {
+                       // We could just use .then() if we didn't have to pass on .abort()…
+                       var apiPromise,
+                               d = $.Deferred();
+
+                       if ( $usernameInput.val().trim() === '' ) {
+                               d.resolve( { valid: true, messages: [] } );
+                               return d.promise();
+                       }
+
+                       apiPromise = api.post( {
+                               action: 'validatepassword',
+                               user: $usernameInput.val(),
+                               password: $passwordInput.val(),
+                               email: $emailInput.val() || '',
+                               realname: $realNameInput.val() || '',
+                               formatversion: 2,
+                               errorformat: 'html',
+                               errorsuselocal: true,
+                               uselang: mw.config.get( 'wgUserLanguage' )
+                       } )
+                               .done( function ( resp ) {
+                                       var pwinfo = resp.validatepassword || {};
+
+                                       d.resolve( {
+                                               valid: pwinfo.validity === 'Good',
+                                               messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
+                                                       return m.html;
+                                               } ) : []
+                                       } );
+                               } )
+                               .fail( d.reject );
+
+                       return d.promise( { abort: apiPromise.abort } );
+               }
+
+               usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
+               usernameChecker.attach();
+
+               passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
+               passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
+       } );
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png
new file mode 100644 (file)
index 0000000..30bf53a
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-contributors.png differ
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png
new file mode 100644 (file)
index 0000000..17508f9
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-edits.png differ
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png
new file mode 100644 (file)
index 0000000..8e37278
Binary files /dev/null and b/resources/src/mediawiki.special.userlogin.signup.styles/images/icon-pages.png differ
diff --git a/resources/src/mediawiki.special.userlogin.signup.styles/signup.css b/resources/src/mediawiki.special.userlogin.signup.styles/signup.css
new file mode 100644 (file)
index 0000000..3cfa5a8
--- /dev/null
@@ -0,0 +1,67 @@
+/* Disable the underline that Vector puts on h2 headings, and bold them. */
+.mw-ui-container h2 {
+       border: 0;
+       font-weight: bold;
+}
+
+/* Benefits column CSS to the right (if it fits) of the form. */
+.mw-ui-container #userloginForm {
+       float: left;
+       /* Override the right margin of the form to give space in case a benefits
+        * column appears to the side. */
+       margin-right: 100px;
+       /* Override `.mw-body-content` to ensure useful, readable paragraphs */
+       line-height: 1.4;
+}
+
+.mw-createacct-benefits-container {
+       /* Keeps this column compact and close to the form, but tends to squish contents. */
+       float: left;
+}
+
+.mw-createacct-benefits-container h2 {
+       margin-bottom: 30px;
+}
+
+.mw-number-text.icon-edits {
+       /* @embed */
+       background: url( images/icon-edits.png ) no-repeat left center;
+}
+
+.mw-number-text.icon-pages {
+       /* @embed */
+       background: url( images/icon-pages.png ) no-repeat left center;
+}
+
+.mw-number-text.icon-contributors {
+       /* @embed */
+       background: url( images/icon-contributors.png ) no-repeat left center;
+}
+
+/*
+ * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
+ * Needs an ID so that it's more specific than Vector's div#content h3.
+ */
+#bodyContent .mw-number-text h3 {
+       color: #222;
+       margin: 0;
+       padding: 0;
+       font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
+       font-weight: normal;
+       font-size: 2.2em;
+       line-height: 1.2;
+       text-align: center;
+}
+
+/* Contains a “headlined” number and explanatory text, with space for an icon */
+.mw-number-text {
+       display: block;
+       font-size: 1.2em;
+       color: #444;
+       margin-top: 1em;
+       /* 80px wide icon plus "margin" */
+       padding: 0 0 0 95px;
+       /* Matches max icon height, ensures icon emblem is visible */
+       min-height: 75px;
+       text-align: center;
+}
diff --git a/resources/src/mediawiki.special.userrights.js b/resources/src/mediawiki.special.userrights.js
new file mode 100644 (file)
index 0000000..487e63a
--- /dev/null
@@ -0,0 +1,25 @@
+/*!
+ * JavaScript for Special:UserRights
+ */
+( function ( mw, $ ) {
+       var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' ),
+               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
+               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
+               $wpReason = $( '#wpReason' );
+
+       // Replace successbox with notifications
+       convertmessagebox();
+
+       // Dynamically show/hide the "other time" input under each dropdown
+       $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
+               $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
+       } );
+
+       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
+       if ( summaryCodePointLimit ) {
+               $wpReason.codePointLimit( summaryCodePointLimit );
+       } else if ( summaryByteLimit ) {
+               $wpReason.byteLimit( summaryByteLimit );
+       }
+
+}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special.version.css b/resources/src/mediawiki.special.version.css
new file mode 100644 (file)
index 0000000..1b8581a
--- /dev/null
@@ -0,0 +1,39 @@
+/*!
+ * Styling for Special:Version
+ */
+.mw-version-ext-name,
+.mw-version-library-name {
+       font-weight: bold;
+}
+
+.mw-version-ext-license,
+.mw-version-ext-vcs-timestamp {
+       white-space: nowrap;
+}
+
+th.mw-version-ext-col-label {
+       font-size: 0.9em;
+}
+
+.mw-version-ext-vcs-version {
+       unicode-bidi: embed;
+}
+
+.mw-version-credits {
+       column-width: 18em;
+       -moz-column-width: 18em;
+       -webkit-column-width: 18em;
+}
+
+.mw-version-credits ul {
+       margin-top: 0;
+       margin-bottom: 0;
+}
+
+.mw-version-license-info strong {
+       font-weight: normal;
+}
+
+.mw-version-license-info em {
+       font-style: normal;
+}
diff --git a/resources/src/mediawiki.special.watchlist.js b/resources/src/mediawiki.special.watchlist.js
new file mode 100644 (file)
index 0000000..565ed2c
--- /dev/null
@@ -0,0 +1,158 @@
+/*!
+ * JavaScript for Special:Watchlist
+ */
+( function ( mw, $, OO ) {
+       $( function () {
+               var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
+
+               // If the user wants to reset their watchlist, use an API call to do so (no reload required)
+               // Adapted from a user script by User:NQ of English Wikipedia
+               // (User:NQ/WatchlistResetConfirm.js)
+               $resetForm.submit( function ( event ) {
+                       var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
+
+                       event.preventDefault();
+
+                       // Disable reset button to prevent multiple concurrent requests
+                       $button.prop( 'disabled', true );
+
+                       if ( !$progressBar ) {
+                               $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
+                               $progressBar.css( {
+                                       position: 'absolute', width: '100%'
+                               } );
+                       }
+                       // Show progress bar
+                       $resetForm.append( $progressBar );
+
+                       // Use action=setnotificationtimestamp to mark all as visited,
+                       // then set all watchlist lines accordingly
+                       api.postWithToken( 'csrf', {
+                               formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
+                       } ).done( function () {
+                               // Enable button again
+                               $button.prop( 'disabled', false );
+                               // Hide the button because further clicks can not generate any visual changes
+                               $button.css( 'visibility', 'hidden' );
+                               $progressBar.detach();
+                               $( '.mw-changeslist-line-watched' )
+                                       .removeClass( 'mw-changeslist-line-watched' )
+                                       .addClass( 'mw-changeslist-line-not-watched' );
+                       } ).fail( function () {
+                               // On error, fall back to server-side reset
+                               // First remove this submit listener and then re-submit the form
+                               $resetForm.off( 'submit' ).submit();
+                       } );
+               } );
+
+               // if the user wishes to reload the watchlist whenever a filter changes
+               if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
+                       // add a listener on all form elements in the header form
+                       $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
+                               // submit the form when one of the input fields is modified
+                               $( '#mw-watchlist-form' ).submit();
+                       } );
+               }
+
+               if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
+                       // Watch/unwatch toggle link:
+                       // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
+                       // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
+                       // Unwatched page entries are struck through and have lowered opacity.
+                       $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
+                               var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
+                                       $watchlistLine = $unwatchLink.closest( 'li, table' )
+                                               .find( '[data-target-page]' ),
+                                       pageTitle = $watchlistLine.data( 'targetPage' ),
+                                       isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
+
+                               // Utility function for looping through each watchlist line that matches
+                               // a certain page or its associated page (e.g. Talk)
+                               function forEachMatchingTitle( title, callback ) {
+
+                                       var titleObj = mw.Title.newFromText( title ),
+                                               pageNamespaceId = titleObj.getNamespaceId(),
+                                               isTalk = pageNamespaceId % 2 === 1,
+                                               associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
+                                                       titleObj.getMainText() ).getPrefixedText();
+                                       $( '.mw-changeslist-line' ).each( function () {
+                                               var $this = $( this ), $row, $unwatchLink;
+
+                                               $this.find( '[data-target-page]' ).each( function () {
+                                                       var $this = $( this ), rowTitle = $this.data( 'targetPage' );
+                                                       if ( rowTitle === title || rowTitle === associatedTitle ) {
+
+                                                               // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
+                                                               // * If using OldChangesList, use the <li>
+                                                               // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
+                                                               // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
+                                                               $row =
+                                                                       $this.closest(
+                                                                               'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
+                                                               $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
+
+                                                               callback( rowTitle, $row, $unwatchLink );
+                                                       }
+                                               } );
+                                       } );
+                               }
+
+                               // Preload the notification module for mw.notify
+                               mw.loader.load( 'mediawiki.notification' );
+
+                               // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
+                               // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
+                               if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
+                                       api.unwatch( pageTitle )
+                                               .done( function () {
+                                                       forEachMatchingTitle( pageTitle,
+                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
+                                                                       $rowUnwatchLink
+                                                                               .text( mw.msg( 'watchlist-unwatch-undo' ) )
+                                                                               .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
+                                                                               .attr( 'href',
+                                                                                       mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
+                                                                               .removeClass( 'mw-unwatch-link loading' )
+                                                                               .addClass( 'mw-watch-link' );
+                                                                       $row.find(
+                                                                               '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
+                                                                               .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
+                                                                               .addClass( 'mw-changelist-line-inner-unwatched' );
+                                                               } );
+
+                                                       mw.notify(
+                                                               mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
+                                                                       pageTitle ), { tag: 'watch-self' } );
+                                               } );
+                               } else {
+                                       api.watch( pageTitle )
+                                               .then( function () {
+                                                       forEachMatchingTitle( pageTitle,
+                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
+                                                                       $rowUnwatchLink
+                                                                               .text( mw.msg( 'watchlist-unwatch' ) )
+                                                                               .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
+                                                                               .attr( 'href',
+                                                                                       mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
+                                                                               .removeClass( 'mw-watch-link loading' )
+                                                                               .addClass( 'mw-unwatch-link' );
+                                                                       $row.find( '.mw-changelist-line-inner-unwatched' )
+                                                                               .addBack( '.mw-enhanced-rc-nested' )
+                                                                               .removeClass( 'mw-changelist-line-inner-unwatched' );
+                                                               } );
+
+                                                       mw.notify(
+                                                               mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
+                                                                       pageTitle ), { tag: 'watch-self' } );
+                                               } );
+                               }
+
+                               event.preventDefault();
+                               event.stopPropagation();
+                               $unwatchLink.blur();
+                       } );
+               }
+       } );
+
+}( mediaWiki, jQuery, OO )
+);
diff --git a/resources/src/mediawiki.special.watchlist.styles.css b/resources/src/mediawiki.special.watchlist.styles.css
new file mode 100644 (file)
index 0000000..c9861c2
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for elements generated by JavaScript on Special:Watchlist
+ */
+.mw-changelist-line-inner-unwatched {
+       text-decoration: line-through;
+       opacity: 0.5;
+}
+
+span.mw-changeslist-line-prefix {
+       display: inline-block;
+}
+/* This can be either a span or a table cell */
+.mw-changeslist-line-prefix {
+       width: 1.25em;
+}
diff --git a/resources/src/mediawiki.special/images/glyph-people-large.png b/resources/src/mediawiki.special/images/glyph-people-large.png
deleted file mode 100644 (file)
index cba3caf..0000000
Binary files a/resources/src/mediawiki.special/images/glyph-people-large.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-contributors.png b/resources/src/mediawiki.special/images/icon-contributors.png
deleted file mode 100644 (file)
index 30bf53a..0000000
Binary files a/resources/src/mediawiki.special/images/icon-contributors.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-edits.png b/resources/src/mediawiki.special/images/icon-edits.png
deleted file mode 100644 (file)
index 17508f9..0000000
Binary files a/resources/src/mediawiki.special/images/icon-edits.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-lock.png b/resources/src/mediawiki.special/images/icon-lock.png
deleted file mode 100644 (file)
index 03f0eec..0000000
Binary files a/resources/src/mediawiki.special/images/icon-lock.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/images/icon-pages.png b/resources/src/mediawiki.special/images/icon-pages.png
deleted file mode 100644 (file)
index 8e37278..0000000
Binary files a/resources/src/mediawiki.special/images/icon-pages.png and /dev/null differ
diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.css b/resources/src/mediawiki.special/mediawiki.special.apisandbox.css
deleted file mode 100644 (file)
index fe5ac41..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-.mw-apisandbox-toolbar {
-       background: #fff;
-       -webkit-position: sticky;
-       position: sticky;
-       top: 0;
-       margin-bottom: -1px;
-       padding: 0.5em 0;
-       border-bottom: 1px solid #a2a9b1;
-       text-align: right;
-       z-index: 1;
-}
-
-#mw-apisandbox-ui .mw-apisandbox-link {
-       display: none;
-}
-
-.mw-apisandbox-popup .oo-ui-popupWidget-body > .oo-ui-widget {
-       vertical-align: middle;
-}
-
-/* So DateTimeInputWidget's calendar popup works... */
-.mw-apisandbox-popup .oo-ui-popupWidget-popup,
-.mw-apisandbox-popup .oo-ui-popupWidget-body {
-       overflow: visible;
-}
-
-/* Display contents of the popup on a single line */
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body {
-       display: table;
-}
-
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > * {
-       display: table-cell;
-}
-
-.mw-apisandbox-popup > .oo-ui-popupWidget-popup > .oo-ui-popupWidget-body > .oo-ui-buttonWidget {
-       padding-left: 0.5em;
-       width: 1%;
-}
-
-.mw-apisandbox-spacer {
-       display: inline-block;
-       height: 1px;
-       width: 5em;
-}
-
-.mw-apisandbox-help-field {
-       border-bottom: 1px solid rgba( 0, 0, 0, 0.1 );
-}
-
-.mw-apisandbox-help-field:last-child {
-       border-bottom: 0;
-}
-
-.mw-apisandbox-optionalWidget {
-       width: 100%;
-}
-
-.mw-apisandbox-optionalWidget.oo-ui-widget-disabled {
-       position: relative;
-       z-index: 0; /* New stacking context to prevent the cover from leaking out */
-}
-
-.mw-apisandbox-optionalWidget-cover {
-       position: absolute;
-       left: 0;
-       right: 0;
-       top: 0;
-       bottom: 0;
-       z-index: 2;
-       cursor: pointer;
-}
-
-.mw-apisandbox-optionalWidget-fields {
-       display: table;
-       width: 100%;
-}
-
-.mw-apisandbox-optionalWidget-widget,
-.mw-apisandbox-optionalWidget-checkbox {
-       display: table-cell;
-       vertical-align: middle;
-}
-
-.mw-apisandbox-optionalWidget-checkbox {
-       width: 1%; /* Will be expanded by content */
-       white-space: nowrap;
-       padding-left: 0.5em;
-}
-
-.mw-apisandbox-textInputCode .oo-ui-inputWidget-input {
-       font-family: monospace, monospace;
-       font-size: 0.8125em;
-       -moz-tab-size: 4;
-       tab-size: 4;
-}
-
-.mw-apisandbox-widget-field .oo-ui-textInputWidget {
-       /* Leave at least enough space for icon, indicator, and a sliver of text */
-       min-width: 6em;
-}
-
-.apihelp-deprecated {
-       font-weight: bold;
-       color: #d33;
-}
-
-.apihelp-deprecated-value .oo-ui-labelElement-label {
-       text-decoration: line-through;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.js b/resources/src/mediawiki.special/mediawiki.special.apisandbox.js
deleted file mode 100644 (file)
index 523a62e..0000000
+++ /dev/null
@@ -1,1864 +0,0 @@
-( function ( $, mw, OO ) {
-       'use strict';
-       var ApiSandbox, Util, WidgetMethods, Validators,
-               $content, panel, booklet, oldhash, windowManager,
-               formatDropdown,
-               api = new mw.Api(),
-               bookletPages = [],
-               availableFormats = {},
-               resultPage = null,
-               suppressErrors = true,
-               updatingBooklet = false,
-               pages = {},
-               moduleInfoCache = {},
-               baseRequestParams;
-
-       /**
-        * A wrapper for a widget that provides an enable/disable button
-        *
-        * @class
-        * @private
-        * @constructor
-        * @param {OO.ui.Widget} widget
-        * @param {Object} [config] Configuration options
-        */
-       function OptionalWidget( widget, config ) {
-               var k;
-
-               config = config || {};
-
-               this.widget = widget;
-               this.$cover = config.$cover ||
-                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-cover' );
-               this.checkbox = new OO.ui.CheckboxInputWidget( config.checkbox )
-                       .on( 'change', this.onCheckboxChange, [], this );
-
-               OptionalWidget[ 'super' ].call( this, config );
-
-               // Forward most methods for convenience
-               for ( k in this.widget ) {
-                       if ( $.isFunction( this.widget[ k ] ) && !this[ k ] ) {
-                               this[ k ] = this.widget[ k ].bind( this.widget );
-                       }
-               }
-
-               this.$cover.on( 'click', this.onOverlayClick.bind( this ) );
-
-               this.$element
-                       .addClass( 'mw-apisandbox-optionalWidget' )
-                       .append(
-                               this.$cover,
-                               $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-fields' ).append(
-                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-widget' ).append(
-                                               widget.$element
-                                       ),
-                                       $( '<div>' ).addClass( 'mw-apisandbox-optionalWidget-checkbox' ).append(
-                                               this.checkbox.$element
-                                       )
-                               )
-                       );
-
-               this.setDisabled( widget.isDisabled() );
-       }
-       OO.inheritClass( OptionalWidget, OO.ui.Widget );
-       OptionalWidget.prototype.onCheckboxChange = function ( checked ) {
-               this.setDisabled( !checked );
-       };
-       OptionalWidget.prototype.onOverlayClick = function () {
-               this.setDisabled( false );
-               if ( $.isFunction( this.widget.focus ) ) {
-                       this.widget.focus();
-               }
-       };
-       OptionalWidget.prototype.setDisabled = function ( disabled ) {
-               OptionalWidget[ 'super' ].prototype.setDisabled.call( this, disabled );
-               this.widget.setDisabled( this.isDisabled() );
-               this.checkbox.setSelected( !this.isDisabled() );
-               this.$cover.toggle( this.isDisabled() );
-               return this;
-       };
-
-       WidgetMethods = {
-               textInputWidget: {
-                       getApiValue: function () {
-                               return this.getValue();
-                       },
-                       setApiValue: function ( v ) {
-                               if ( v === undefined ) {
-                                       v = this.paramInfo[ 'default' ];
-                               }
-                               this.setValue( v );
-                       },
-                       apiCheckValid: function () {
-                               var that = this;
-                               return this.getValidity().then( function () {
-                                       return $.Deferred().resolve( true ).promise();
-                               }, function () {
-                                       return $.Deferred().resolve( false ).promise();
-                               } ).done( function ( ok ) {
-                                       ok = ok || suppressErrors;
-                                       that.setIcon( ok ? null : 'alert' );
-                                       that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               } );
-                       }
-               },
-
-               dateTimeInputWidget: {
-                       getValidity: function () {
-                               if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) {
-                                       return $.Deferred().resolve().promise();
-                               } else {
-                                       return $.Deferred().reject().promise();
-                               }
-                       }
-               },
-
-               tokenWidget: {
-                       alertTokenError: function ( code, error ) {
-                               windowManager.openWindow( 'errorAlert', {
-                                       title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
-                                       message: error,
-                                       actions: [
-                                               {
-                                                       action: 'accept',
-                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
-                                                       flags: 'primary'
-                                               }
-                                       ]
-                               } );
-                       },
-                       fetchToken: function () {
-                               this.pushPending();
-                               return api.getToken( this.paramInfo.tokentype )
-                                       .done( this.setApiValue.bind( this ) )
-                                       .fail( this.alertTokenError.bind( this ) )
-                                       .always( this.popPending.bind( this ) );
-                       },
-                       setApiValue: function ( v ) {
-                               WidgetMethods.textInputWidget.setApiValue.call( this, v );
-                               if ( v === '123ABC' ) {
-                                       this.fetchToken();
-                               }
-                       }
-               },
-
-               passwordWidget: {
-                       getApiValueForDisplay: function () {
-                               return '';
-                       }
-               },
-
-               toggleSwitchWidget: {
-                       getApiValue: function () {
-                               return this.getValue() ? 1 : undefined;
-                       },
-                       setApiValue: function ( v ) {
-                               this.setValue( Util.apiBool( v ) );
-                       },
-                       apiCheckValid: function () {
-                               return $.Deferred().resolve( true ).promise();
-                       }
-               },
-
-               dropdownWidget: {
-                       getApiValue: function () {
-                               var item = this.getMenu().findSelectedItem();
-                               return item === null ? undefined : item.getData();
-                       },
-                       setApiValue: function ( v ) {
-                               var menu = this.getMenu();
-
-                               if ( v === undefined ) {
-                                       v = this.paramInfo[ 'default' ];
-                               }
-                               if ( v === undefined ) {
-                                       menu.selectItem();
-                               } else {
-                                       menu.selectItemByData( String( v ) );
-                               }
-                       },
-                       apiCheckValid: function () {
-                               var ok = this.getApiValue() !== undefined || suppressErrors;
-                               this.setIcon( ok ? null : 'alert' );
-                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               return $.Deferred().resolve( ok ).promise();
-                       }
-               },
-
-               tagWidget: {
-                       getApiValue: function () {
-                               var items = this.getValue();
-                               if ( items.join( '' ).indexOf( '|' ) === -1 ) {
-                                       return items.join( '|' );
-                               } else {
-                                       return '\x1f' + items.join( '\x1f' );
-                               }
-                       },
-                       setApiValue: function ( v ) {
-                               if ( v === undefined || v === '' || v === '\x1f' ) {
-                                       this.setValue( [] );
-                               } else {
-                                       v = String( v );
-                                       if ( v.indexOf( '\x1f' ) !== 0 ) {
-                                               this.setValue( v.split( '|' ) );
-                                       } else {
-                                               this.setValue( v.substr( 1 ).split( '\x1f' ) );
-                                       }
-                               }
-                       },
-                       apiCheckValid: function () {
-                               var ok = true,
-                                       pi = this.paramInfo;
-
-                               if ( !suppressErrors ) {
-                                       ok = this.getApiValue() !== undefined && !(
-                                               pi.allspecifier !== undefined &&
-                                               this.getValue().length > 1 &&
-                                               this.getValue().indexOf( pi.allspecifier ) !== -1
-                                       );
-                               }
-
-                               this.setIcon( ok ? null : 'alert' );
-                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               return $.Deferred().resolve( ok ).promise();
-                       },
-                       createTagItemWidget: function ( data, label ) {
-                               var item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
-                               if ( this.paramInfo.deprecatedvalues &&
-                                       this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
-                               ) {
-                                       item.$element.addClass( 'apihelp-deprecated-value' );
-                               }
-                               return item;
-                       }
-               },
-
-               optionalWidget: {
-                       getApiValue: function () {
-                               return this.isDisabled() ? undefined : this.widget.getApiValue();
-                       },
-                       setApiValue: function ( v ) {
-                               this.setDisabled( v === undefined );
-                               this.widget.setApiValue( v );
-                       },
-                       apiCheckValid: function () {
-                               if ( this.isDisabled() ) {
-                                       return $.Deferred().resolve( true ).promise();
-                               } else {
-                                       return this.widget.apiCheckValid();
-                               }
-                       }
-               },
-
-               submoduleWidget: {
-                       single: function () {
-                               var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
-                               return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
-                       },
-                       multi: function () {
-                               var map = this.paramInfo.submodules,
-                                       v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue();
-                               return v === undefined || v === '' ? [] : String( v ).split( '|' ).map( function ( v ) {
-                                       return { value: v, path: map[ v ] };
-                               } );
-                       }
-               },
-
-               uploadWidget: {
-                       getApiValueForDisplay: function () {
-                               return '...';
-                       },
-                       getApiValue: function () {
-                               return this.getValue();
-                       },
-                       setApiValue: function () {
-                               // Can't, sorry.
-                       },
-                       apiCheckValid: function () {
-                               var ok = this.getValue() !== null || suppressErrors;
-                               this.setIcon( ok ? null : 'alert' );
-                               this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
-                               return $.Deferred().resolve( ok ).promise();
-                       }
-               }
-       };
-
-       Validators = {
-               generic: function () {
-                       return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
-               }
-       };
-
-       /**
-        * @class mw.special.ApiSandbox.Util
-        * @private
-        */
-       Util = {
-               /**
-                * Fetch API module info
-                *
-                * @param {string} module Module to fetch data for
-                * @return {jQuery.Promise}
-                */
-               fetchModuleInfo: function ( module ) {
-                       var apiPromise,
-                               deferred = $.Deferred();
-
-                       if ( moduleInfoCache.hasOwnProperty( module ) ) {
-                               return deferred
-                                       .resolve( moduleInfoCache[ module ] )
-                                       .promise( { abort: function () {} } );
-                       } else {
-                               apiPromise = api.post( {
-                                       action: 'paraminfo',
-                                       modules: module,
-                                       helpformat: 'html',
-                                       uselang: mw.config.get( 'wgUserLanguage' )
-                               } ).done( function ( data ) {
-                                       var info;
-
-                                       if ( data.warnings && data.warnings.paraminfo ) {
-                                               deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
-                                               return;
-                                       }
-
-                                       info = data.paraminfo.modules;
-                                       if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
-                                               deferred.reject( '???', 'No module data returned' );
-                                               return;
-                                       }
-
-                                       moduleInfoCache[ module ] = info[ 0 ];
-                                       deferred.resolve( info[ 0 ] );
-                               } ).fail( function ( code, details ) {
-                                       if ( code === 'http' ) {
-                                               details = 'HTTP error: ' + details.exception;
-                                       } else if ( details.error ) {
-                                               details = details.error.info;
-                                       }
-                                       deferred.reject( code, details );
-                               } );
-                               return deferred
-                                       .promise( { abort: apiPromise.abort } );
-                       }
-               },
-
-               /**
-                * Mark all currently-in-use tokens as bad
-                */
-               markTokensBad: function () {
-                       var page, subpages, i,
-                               checkPages = [ pages.main ];
-
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-
-                               if ( page.tokenWidget ) {
-                                       api.badToken( page.tokenWidget.paramInfo.tokentype );
-                               }
-
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-               },
-
-               /**
-                * Test an API boolean
-                *
-                * @param {Mixed} value
-                * @return {boolean}
-                */
-               apiBool: function ( value ) {
-                       return value !== undefined && value !== false;
-               },
-
-               /**
-                * Create a widget for a parameter.
-                *
-                * @param {Object} pi Parameter info from API
-                * @param {Object} opts Additional options
-                * @return {OO.ui.Widget}
-                */
-               createWidgetForParameter: function ( pi, opts ) {
-                       var widget, innerWidget, finalWidget, items, $content, func,
-                               multiModeButton = null,
-                               multiModeInput = null,
-                               multiModeAllowed = false;
-
-                       opts = opts || {};
-
-                       switch ( pi.type ) {
-                               case 'boolean':
-                                       widget = new OO.ui.ToggleSwitchWidget();
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.toggleSwitchWidget );
-                                       pi.required = true; // Avoid wrapping in the non-required widget
-                                       break;
-
-                               case 'string':
-                               case 'user':
-                                       if ( Util.apiBool( pi.multi ) ) {
-                                               widget = new OO.ui.TagMultiselectWidget( {
-                                                       allowArbitrary: true,
-                                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.tagWidget );
-                                       } else {
-                                               widget = new OO.ui.TextInputWidget( {
-                                                       required: Util.apiBool( pi.required )
-                                               } );
-                                       }
-                                       if ( !Util.apiBool( pi.multi ) ) {
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.textInputWidget );
-                                               widget.setValidation( Validators.generic );
-                                       }
-                                       if ( pi.tokentype ) {
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.textInputWidget );
-                                               $.extend( widget, WidgetMethods.tokenWidget );
-                                       }
-                                       break;
-
-                               case 'text':
-                                       widget = new OO.ui.MultilineTextInputWidget( {
-                                               required: Util.apiBool( pi.required )
-                                       } );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       widget.setValidation( Validators.generic );
-                                       break;
-
-                               case 'password':
-                                       widget = new OO.ui.TextInputWidget( {
-                                               type: 'password',
-                                               required: Util.apiBool( pi.required )
-                                       } );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       $.extend( widget, WidgetMethods.passwordWidget );
-                                       widget.setValidation( Validators.generic );
-                                       multiModeAllowed = true;
-                                       multiModeInput = widget;
-                                       break;
-
-                               case 'integer':
-                                       widget = new OO.ui.NumberInputWidget( {
-                                               required: Util.apiBool( pi.required ),
-                                               isInteger: true
-                                       } );
-                                       widget.setIcon = widget.input.setIcon.bind( widget.input );
-                                       widget.setIconTitle = widget.input.setIconTitle.bind( widget.input );
-                                       widget.getValidity = widget.input.getValidity.bind( widget.input );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       if ( Util.apiBool( pi.enforcerange ) ) {
-                                               widget.setRange( pi.min || -Infinity, pi.max || Infinity );
-                                       }
-                                       multiModeAllowed = true;
-                                       multiModeInput = widget;
-                                       break;
-
-                               case 'limit':
-                                       widget = new OO.ui.TextInputWidget( {
-                                               required: Util.apiBool( pi.required )
-                                       } );
-                                       widget.setValidation( function ( value ) {
-                                               var n, pi = this.paramInfo;
-
-                                               if ( value === 'max' ) {
-                                                       return true;
-                                               } else {
-                                                       n = +value;
-                                                       return !isNaN( n ) && isFinite( n ) &&
-                                                               Math.floor( n ) === n &&
-                                                               n >= pi.min && n <= pi.apiSandboxMax;
-                                               }
-                                       } );
-                                       pi.min = pi.min || 0;
-                                       pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max;
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       multiModeAllowed = true;
-                                       multiModeInput = widget;
-                                       break;
-
-                               case 'timestamp':
-                                       widget = new mw.widgets.datetime.DateTimeInputWidget( {
-                                               formatter: {
-                                                       format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}'
-                                               },
-                                               required: Util.apiBool( pi.required ),
-                                               clearable: false
-                                       } );
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.textInputWidget );
-                                       $.extend( widget, WidgetMethods.dateTimeInputWidget );
-                                       multiModeAllowed = true;
-                                       break;
-
-                               case 'upload':
-                                       widget = new OO.ui.SelectFileWidget();
-                                       widget.paramInfo = pi;
-                                       $.extend( widget, WidgetMethods.uploadWidget );
-                                       break;
-
-                               case 'namespace':
-                                       items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) {
-                                               if ( ns === '0' ) {
-                                                       name = mw.message( 'blanknamespace' ).text();
-                                               }
-                                               return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
-                                       } ).sort( function ( a, b ) {
-                                               return a.data - b.data;
-                                       } );
-                                       if ( Util.apiBool( pi.multi ) ) {
-                                               if ( pi.allspecifier !== undefined ) {
-                                                       items.unshift( new OO.ui.MenuOptionWidget( {
-                                                               data: pi.allspecifier,
-                                                               label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text()
-                                                       } ) );
-                                               }
-
-                                               widget = new OO.ui.MenuTagMultiselectWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.tagWidget );
-                                       } else {
-                                               widget = new OO.ui.DropdownWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.dropdownWidget );
-                                       }
-                                       break;
-
-                               default:
-                                       if ( !Array.isArray( pi.type ) ) {
-                                               throw new Error( 'Unknown parameter type ' + pi.type );
-                                       }
-
-                                       items = pi.type.map( function ( v ) {
-                                               var config = {
-                                                       data: String( v ),
-                                                       label: String( v ),
-                                                       classes: []
-                                               };
-                                               if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
-                                                       config.classes.push( 'apihelp-deprecated-value' );
-                                               }
-                                               return new OO.ui.MenuOptionWidget( config );
-                                       } );
-                                       if ( Util.apiBool( pi.multi ) ) {
-                                               if ( pi.allspecifier !== undefined ) {
-                                                       items.unshift( new OO.ui.MenuOptionWidget( {
-                                                               data: pi.allspecifier,
-                                                               label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text()
-                                                       } ) );
-                                               }
-
-                                               widget = new OO.ui.MenuTagMultiselectWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.tagWidget );
-                                               if ( Util.apiBool( pi.submodules ) ) {
-                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
-                                                       widget.on( 'change', ApiSandbox.updateUI );
-                                               }
-                                       } else {
-                                               widget = new OO.ui.DropdownWidget( {
-                                                       menu: { items: items },
-                                                       $overlay: true
-                                               } );
-                                               widget.paramInfo = pi;
-                                               $.extend( widget, WidgetMethods.dropdownWidget );
-                                               if ( Util.apiBool( pi.submodules ) ) {
-                                                       widget.getSubmodules = WidgetMethods.submoduleWidget.single;
-                                                       widget.getMenu().on( 'select', ApiSandbox.updateUI );
-                                               }
-                                               if ( pi.deprecatedvalues ) {
-                                                       widget.getMenu().on( 'select', function ( item ) {
-                                                               this.$element.toggleClass(
-                                                                       'apihelp-deprecated-value',
-                                                                       pi.deprecatedvalues.indexOf( item.data ) >= 0
-                                                               );
-                                                       }, [], widget );
-                                               }
-                                       }
-
-                                       break;
-                       }
-
-                       if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
-                               innerWidget = widget;
-
-                               multiModeButton = new OO.ui.ButtonWidget( {
-                                       label: mw.message( 'apisandbox-add-multi' ).text()
-                               } );
-                               $content = innerWidget.$element.add( multiModeButton.$element );
-
-                               widget = new OO.ui.PopupTagMultiselectWidget( {
-                                       allowArbitrary: true,
-                                       allowDuplicates: Util.apiBool( pi.allowsduplicates ),
-                                       $overlay: true,
-                                       popup: {
-                                               classes: [ 'mw-apisandbox-popup' ],
-                                               padded: true,
-                                               $content: $content
-                                       }
-                               } );
-                               widget.paramInfo = pi;
-                               $.extend( widget, WidgetMethods.tagWidget );
-
-                               func = function () {
-                                       if ( !innerWidget.isDisabled() ) {
-                                               innerWidget.apiCheckValid().done( function ( ok ) {
-                                                       if ( ok ) {
-                                                               widget.addTag( innerWidget.getApiValue() );
-                                                               innerWidget.setApiValue( undefined );
-                                                       }
-                                               } );
-                                               return false;
-                                       }
-                               };
-
-                               if ( multiModeInput ) {
-                                       multiModeInput.on( 'enter', func );
-                               }
-                               multiModeButton.on( 'click', func );
-                       }
-
-                       if ( Util.apiBool( pi.required ) || opts.nooptional ) {
-                               finalWidget = widget;
-                       } else {
-                               finalWidget = new OptionalWidget( widget );
-                               finalWidget.paramInfo = pi;
-                               $.extend( finalWidget, WidgetMethods.optionalWidget );
-                               if ( widget.getSubmodules ) {
-                                       finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
-                                       finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } );
-                               }
-                               finalWidget.setDisabled( true );
-                       }
-
-                       widget.setApiValue( pi[ 'default' ] );
-
-                       return finalWidget;
-               },
-
-               /**
-                * Parse an HTML string and call Util.fixupHTML()
-                *
-                * @param {string} html HTML to parse
-                * @return {jQuery}
-                */
-               parseHTML: function ( html ) {
-                       var $ret = $( $.parseHTML( html ) );
-                       return Util.fixupHTML( $ret );
-               },
-
-               /**
-                * Parse an i18n message and call Util.fixupHTML()
-                *
-                * @param {string} key Key of message to get
-                * @param {...Mixed} parameters Values for $N replacements
-                * @return {jQuery}
-                */
-               parseMsg: function () {
-                       var $ret = mw.message.apply( mw.message, arguments ).parseDom();
-                       return Util.fixupHTML( $ret );
-               },
-
-               /**
-                * Fix HTML for ApiSandbox display
-                *
-                * Fixes are:
-                * - Add target="_blank" to any links
-                *
-                * @param {jQuery} $html DOM to process
-                * @return {jQuery}
-                */
-               fixupHTML: function ( $html ) {
-                       $html.filter( 'a' ).add( $html.find( 'a' ) )
-                               .filter( '[href]:not([target])' )
-                               .attr( 'target', '_blank' );
-                       return $html;
-               },
-
-               /**
-                * Format a request and return a bunch of menu option widgets
-                *
-                * @param {Object} displayParams Query parameters, sanitized for display.
-                * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
-                * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
-                */
-               formatRequest: function ( displayParams, rawParams ) {
-                       var jsonInput,
-                               items = [
-                                       new OO.ui.MenuOptionWidget( {
-                                               label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
-                                               data: new OO.ui.FieldLayout(
-                                                       new OO.ui.TextInputWidget( {
-                                                               readOnly: true,
-                                                               value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams )
-                                                       } ), {
-                                                               label: Util.parseMsg( 'apisandbox-request-url-label' )
-                                                       }
-                                               )
-                                       } ),
-                                       new OO.ui.MenuOptionWidget( {
-                                               label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
-                                               data: new OO.ui.FieldLayout(
-                                                       jsonInput = new OO.ui.MultilineTextInputWidget( {
-                                                               classes: [ 'mw-apisandbox-textInputCode' ],
-                                                               readOnly: true,
-                                                               autosize: true,
-                                                               maxRows: 6,
-                                                               value: JSON.stringify( displayParams, null, '\t' )
-                                                       } ), {
-                                                               label: Util.parseMsg( 'apisandbox-request-json-label' )
-                                                       }
-                                               ).on( 'toggle', function ( visible ) {
-                                                       if ( visible ) {
-                                                               // Call updatePosition instead of adjustSize
-                                                               // because the latter has weird caching
-                                                               // behavior and the former bypasses it.
-                                                               jsonInput.updatePosition();
-                                                       }
-                                               } )
-                                       } )
-                               ];
-
-                       mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams );
-
-                       return items;
-               },
-
-               /**
-                * Event handler for when formatDropdown's selection changes
-                */
-               onFormatDropdownChange: function () {
-                       var i,
-                               menu = formatDropdown.getMenu(),
-                               items = menu.getItems(),
-                               selectedField = menu.findSelectedItem() ? menu.findSelectedItem().getData() : null;
-
-                       for ( i = 0; i < items.length; i++ ) {
-                               items[ i ].getData().toggle( items[ i ].getData() === selectedField );
-                       }
-               }
-       };
-
-       /**
-       * Interface to ApiSandbox UI
-       *
-       * @class mw.special.ApiSandbox
-       */
-       ApiSandbox = {
-               /**
-                * Initialize the UI
-                *
-                * Automatically called on $.ready()
-                */
-               init: function () {
-                       var $toolbar;
-
-                       $content = $( '#mw-apisandbox' );
-
-                       windowManager = new OO.ui.WindowManager();
-                       $( 'body' ).append( windowManager.$element );
-                       windowManager.addWindows( {
-                               errorAlert: new OO.ui.MessageDialog()
-                       } );
-
-                       $toolbar = $( '<div>' )
-                               .addClass( 'mw-apisandbox-toolbar' )
-                               .append(
-                                       new OO.ui.ButtonWidget( {
-                                               label: mw.message( 'apisandbox-submit' ).text(),
-                                               flags: [ 'primary', 'progressive' ]
-                                       } ).on( 'click', ApiSandbox.sendRequest ).$element,
-                                       new OO.ui.ButtonWidget( {
-                                               label: mw.message( 'apisandbox-reset' ).text(),
-                                               flags: 'destructive'
-                                       } ).on( 'click', ApiSandbox.resetUI ).$element
-                               );
-
-                       booklet = new OO.ui.BookletLayout( {
-                               expanded: false,
-                               outlined: true,
-                               autoFocus: false
-                       } );
-
-                       panel = new OO.ui.PanelLayout( {
-                               classes: [ 'mw-apisandbox-container' ],
-                               content: [ booklet ],
-                               expanded: false,
-                               framed: true
-                       } );
-
-                       pages.main = new ApiSandbox.PageLayout( { key: 'main', path: 'main' } );
-
-                       // Parse the current hash string
-                       if ( !ApiSandbox.loadFromHash() ) {
-                               ApiSandbox.updateUI();
-                       }
-
-                       $( window ).on( 'hashchange', ApiSandbox.loadFromHash );
-
-                       $content
-                               .empty()
-                               .append( $( '<p>' ).append( Util.parseMsg( 'apisandbox-intro' ) ) )
-                               .append(
-                                       $( '<div>' ).attr( 'id', 'mw-apisandbox-ui' )
-                                               .append( $toolbar )
-                                               .append( panel.$element )
-                               );
-               },
-
-               /**
-                * Update the current query when the page hash changes
-                *
-                * @return {boolean} Successful
-                */
-               loadFromHash: function () {
-                       var params, m, re,
-                               hash = location.hash;
-
-                       if ( oldhash === hash ) {
-                               return false;
-                       }
-                       oldhash = hash;
-                       if ( hash === '' ) {
-                               return false;
-                       }
-
-                       // I'm surprised this doesn't seem to exist in jQuery or mw.util.
-                       params = {};
-                       hash = hash.replace( /\+/g, '%20' );
-                       re = /([^&=#]+)=?([^&#]*)/g;
-                       while ( ( m = re.exec( hash ) ) ) {
-                               params[ decodeURIComponent( m[ 1 ] ) ] = decodeURIComponent( m[ 2 ] );
-                       }
-
-                       ApiSandbox.updateUI( params );
-                       return true;
-               },
-
-               /**
-                * Update the pages in the booklet
-                *
-                * @param {Object} [params] Optional query parameters to load
-                */
-               updateUI: function ( params ) {
-                       var i, page, subpages, j, removePages,
-                               addPages = [];
-
-                       if ( !$.isPlainObject( params ) ) {
-                               params = undefined;
-                       }
-
-                       if ( updatingBooklet ) {
-                               return;
-                       }
-                       updatingBooklet = true;
-                       try {
-                               if ( params !== undefined ) {
-                                       pages.main.loadQueryParams( params );
-                               }
-                               addPages.push( pages.main );
-                               if ( resultPage !== null ) {
-                                       addPages.push( resultPage );
-                               }
-                               pages.main.apiCheckValid();
-
-                               i = 0;
-                               while ( addPages.length ) {
-                                       page = addPages.shift();
-                                       if ( bookletPages[ i ] !== page ) {
-                                               for ( j = i; j < bookletPages.length; j++ ) {
-                                                       if ( bookletPages[ j ].getName() === page.getName() ) {
-                                                               bookletPages.splice( j, 1 );
-                                                       }
-                                               }
-                                               bookletPages.splice( i, 0, page );
-                                               booklet.addPages( [ page ], i );
-                                       }
-                                       i++;
-
-                                       if ( page.getSubpages ) {
-                                               subpages = page.getSubpages();
-                                               for ( j = 0; j < subpages.length; j++ ) {
-                                                       if ( !pages.hasOwnProperty( subpages[ j ].key ) ) {
-                                                               subpages[ j ].indentLevel = page.indentLevel + 1;
-                                                               pages[ subpages[ j ].key ] = new ApiSandbox.PageLayout( subpages[ j ] );
-                                                       }
-                                                       if ( params !== undefined ) {
-                                                               pages[ subpages[ j ].key ].loadQueryParams( params );
-                                                       }
-                                                       addPages.splice( j, 0, pages[ subpages[ j ].key ] );
-                                                       pages[ subpages[ j ].key ].apiCheckValid();
-                                               }
-                                       }
-                               }
-
-                               if ( bookletPages.length > i ) {
-                                       removePages = bookletPages.splice( i, bookletPages.length - i );
-                                       booklet.removePages( removePages );
-                               }
-
-                               if ( !booklet.getCurrentPageName() ) {
-                                       booklet.selectFirstSelectablePage();
-                               }
-                       } finally {
-                               updatingBooklet = false;
-                       }
-               },
-
-               /**
-                * Reset button handler
-                */
-               resetUI: function () {
-                       suppressErrors = true;
-                       pages = {
-                               main: new ApiSandbox.PageLayout( { key: 'main', path: 'main' } )
-                       };
-                       resultPage = null;
-                       ApiSandbox.updateUI();
-               },
-
-               /**
-                * Submit button handler
-                *
-                * @param {Object} [params] Use this set of params instead of those in the form fields.
-                *   The form fields will be updated to match.
-                */
-               sendRequest: function ( params ) {
-                       var page, subpages, i, query, $result, $focus,
-                               progress, $progressText, progressLoading,
-                               deferreds = [],
-                               paramsAreForced = !!params,
-                               displayParams = {},
-                               tokenWidgets = [],
-                               checkPages = [ pages.main ];
-
-                       // Blur any focused widget before submit, because
-                       // OO.ui.ButtonWidget doesn't take focus itself (T128054)
-                       $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
-                       if ( $focus.length ) {
-                               $focus[ 0 ].blur();
-                       }
-
-                       suppressErrors = false;
-
-                       // save widget state in params (or load from it if we are forced)
-                       if ( paramsAreForced ) {
-                               ApiSandbox.updateUI( params );
-                       }
-                       params = {};
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-                               if ( page.tokenWidget ) {
-                                       tokenWidgets.push( page.tokenWidget );
-                               }
-                               deferreds = deferreds.concat( page.apiCheckValid() );
-                               page.getQueryParams( params, displayParams );
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-
-                       if ( !paramsAreForced ) {
-                               // forced params means we are continuing a query; the base query should be preserved
-                               baseRequestParams = $.extend( {}, params );
-                       }
-
-                       $.when.apply( $, deferreds ).done( function () {
-                               var formatItems, menu, selectedLabel, deferred, actions, errorCount;
-
-                               // Count how many times `value` occurs in `array`.
-                               function countValues( value, array ) {
-                                       var count, i;
-                                       count = 0;
-                                       for ( i = 0; i < array.length; i++ ) {
-                                               if ( array[ i ] === value ) {
-                                                       count++;
-                                               }
-                                       }
-                                       return count;
-                               }
-
-                               errorCount = countValues( false, arguments );
-                               if ( errorCount > 0 ) {
-                                       actions = [
-                                               {
-                                                       action: 'accept',
-                                                       label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
-                                                       flags: 'primary'
-                                               }
-                                       ];
-                                       if ( tokenWidgets.length ) {
-                                               // Check all token widgets' validity separately
-                                               deferred = $.when.apply( $, tokenWidgets.map( function ( w ) {
-                                                       return w.apiCheckValid();
-                                               } ) );
-
-                                               deferred.done( function () {
-                                                       // If only the tokens are invalid, offer to fix them
-                                                       var tokenErrorCount = countValues( false, arguments );
-                                                       if ( tokenErrorCount === errorCount ) {
-                                                               delete actions[ 0 ].flags;
-                                                               actions.push( {
-                                                                       action: 'fix',
-                                                                       label: mw.message( 'apisandbox-results-fixtoken' ).text(),
-                                                                       flags: 'primary'
-                                                               } );
-                                                       }
-                                               } );
-                                       } else {
-                                               deferred = $.Deferred().resolve();
-                                       }
-                                       deferred.always( function () {
-                                               windowManager.openWindow( 'errorAlert', {
-                                                       title: Util.parseMsg( 'apisandbox-submit-invalid-fields-title' ),
-                                                       message: Util.parseMsg( 'apisandbox-submit-invalid-fields-message' ),
-                                                       actions: actions
-                                               } ).closed.then( function ( data ) {
-                                                       if ( data && data.action === 'fix' ) {
-                                                               ApiSandbox.fixTokenAndResend();
-                                                       }
-                                               } );
-                                       } );
-                                       return;
-                               }
-
-                               query = $.param( displayParams );
-
-                               formatItems = Util.formatRequest( displayParams, params );
-
-                               // Force a 'fm' format with wrappedhtml=1, if available
-                               if ( params.format !== undefined ) {
-                                       if ( availableFormats.hasOwnProperty( params.format + 'fm' ) ) {
-                                               params.format = params.format + 'fm';
-                                       }
-                                       if ( params.format.substr( -2 ) === 'fm' ) {
-                                               params.wrappedhtml = 1;
-                                       }
-                               }
-
-                               progressLoading = false;
-                               $progressText = $( '<span>' ).text( mw.message( 'apisandbox-sending-request' ).text() );
-                               progress = new OO.ui.ProgressBarWidget( {
-                                       progress: false,
-                                       $content: $progressText
-                               } );
-
-                               $result = $( '<div>' )
-                                       .append( progress.$element );
-
-                               resultPage = page = new OO.ui.PageLayout( '|results|', { expanded: false } );
-                               page.setupOutlineItem = function () {
-                                       this.outlineItem.setLabel( mw.message( 'apisandbox-results' ).text() );
-                               };
-
-                               if ( !formatDropdown ) {
-                                       formatDropdown = new OO.ui.DropdownWidget( {
-                                               menu: { items: [] },
-                                               $overlay: true
-                                       } );
-                                       formatDropdown.getMenu().on( 'select', Util.onFormatDropdownChange );
-                               }
-
-                               menu = formatDropdown.getMenu();
-                               selectedLabel = menu.findSelectedItem() ? menu.findSelectedItem().getLabel() : '';
-                               if ( typeof selectedLabel !== 'string' ) {
-                                       selectedLabel = selectedLabel.text();
-                               }
-                               menu.clearItems().addItems( formatItems );
-                               menu.chooseItem( menu.getItemFromLabel( selectedLabel ) || menu.findFirstSelectableItem() );
-
-                               // Fire the event to update field visibilities
-                               Util.onFormatDropdownChange();
-
-                               page.$element.empty()
-                                       .append(
-                                               new OO.ui.FieldLayout(
-                                                       formatDropdown, {
-                                                               label: Util.parseMsg( 'apisandbox-request-selectformat-label' )
-                                                       }
-                                               ).$element,
-                                               formatItems.map( function ( item ) {
-                                                       return item.getData().$element;
-                                               } ),
-                                               $result
-                                       );
-                               ApiSandbox.updateUI();
-                               booklet.setPage( '|results|' );
-
-                               location.href = oldhash = '#' + query;
-
-                               api.post( params, {
-                                       contentType: 'multipart/form-data',
-                                       dataType: 'text',
-                                       xhr: function () {
-                                               var xhr = new window.XMLHttpRequest();
-                                               xhr.upload.addEventListener( 'progress', function ( e ) {
-                                                       if ( !progressLoading ) {
-                                                               if ( e.lengthComputable ) {
-                                                                       progress.setProgress( e.loaded * 100 / e.total );
-                                                               } else {
-                                                                       progress.setProgress( false );
-                                                               }
-                                                       }
-                                               } );
-                                               xhr.addEventListener( 'progress', function ( e ) {
-                                                       if ( !progressLoading ) {
-                                                               progressLoading = true;
-                                                               $progressText.text( mw.message( 'apisandbox-loading-results' ).text() );
-                                                       }
-                                                       if ( e.lengthComputable ) {
-                                                               progress.setProgress( e.loaded * 100 / e.total );
-                                                       } else {
-                                                               progress.setProgress( false );
-                                                       }
-                                               } );
-                                               return xhr;
-                                       }
-                               } )
-                                       .catch( function ( code, data, result, jqXHR ) {
-                                               var deferred = $.Deferred();
-
-                                               if ( code !== 'http' ) {
-                                                       // Not really an error, work around mw.Api thinking it is.
-                                                       deferred.resolve( result, jqXHR );
-                                               } else {
-                                                       // Just forward it.
-                                                       deferred.reject.apply( deferred, arguments );
-                                               }
-                                               return deferred.promise();
-                                       } )
-                                       .then( function ( data, jqXHR ) {
-                                               var m, loadTime, button, clear,
-                                                       ct = jqXHR.getResponseHeader( 'Content-Type' ),
-                                                       loginSuppressed = jqXHR.getResponseHeader( 'MediaWiki-Login-Suppressed' ) || 'false';
-
-                                               $result.empty();
-                                               if ( loginSuppressed !== 'false' ) {
-                                                       $( '<div>' )
-                                                               .addClass( 'warning' )
-                                                               .append( Util.parseMsg( 'apisandbox-results-login-suppressed' ) )
-                                                               .appendTo( $result );
-                                               }
-                                               if ( /^text\/mediawiki-api-prettyprint-wrapped(?:;|$)/.test( ct ) ) {
-                                                       data = JSON.parse( data );
-                                                       if ( data.modules.length ) {
-                                                               mw.loader.load( data.modules );
-                                                       }
-                                                       if ( data.status && data.status !== 200 ) {
-                                                               $( '<div>' )
-                                                                       .addClass( 'api-pretty-header api-pretty-status' )
-                                                                       .append( Util.parseMsg( 'api-format-prettyprint-status', data.status, data.statustext ) )
-                                                                       .appendTo( $result );
-                                                       }
-                                                       $result.append( Util.parseHTML( data.html ) );
-                                                       loadTime = data.time;
-                                               } else if ( ( m = data.match( /<pre[ >][\s\S]*<\/pre>/ ) ) ) {
-                                                       $result.append( Util.parseHTML( m[ 0 ] ) );
-                                                       if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) {
-                                                               loadTime = parseInt( m[ 1 ], 10 );
-                                                       }
-                                               } else {
-                                                       $( '<pre>' )
-                                                               .addClass( 'api-pretty-content' )
-                                                               .text( data )
-                                                               .appendTo( $result );
-                                               }
-                                               if ( paramsAreForced || data[ 'continue' ] ) {
-                                                       $result.append(
-                                                               $( '<div>' ).append(
-                                                                       new OO.ui.ButtonWidget( {
-                                                                               label: mw.message( 'apisandbox-continue' ).text()
-                                                                       } ).on( 'click', function () {
-                                                                               ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) );
-                                                                       } ).setDisabled( !data[ 'continue' ] ).$element,
-                                                                       ( clear = new OO.ui.ButtonWidget( {
-                                                                               label: mw.message( 'apisandbox-continue-clear' ).text()
-                                                                       } ).on( 'click', function () {
-                                                                               ApiSandbox.updateUI( baseRequestParams );
-                                                                               clear.setDisabled( true );
-                                                                               booklet.setPage( '|results|' );
-                                                                       } ).setDisabled( !paramsAreForced ) ).$element,
-                                                                       new OO.ui.PopupButtonWidget( {
-                                                                               $overlay: true,
-                                                                               framed: false,
-                                                                               icon: 'info',
-                                                                               popup: {
-                                                                                       $content: $( '<div>' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ),
-                                                                                       padded: true,
-                                                                                       width: 'auto'
-                                                                               }
-                                                                       } ).$element
-                                                               )
-                                                       );
-                                               }
-                                               if ( typeof loadTime === 'number' ) {
-                                                       $result.append(
-                                                               $( '<div>' ).append(
-                                                                       new OO.ui.LabelWidget( {
-                                                                               label: mw.message( 'apisandbox-request-time', loadTime ).text()
-                                                                       } ).$element
-                                                               )
-                                                       );
-                                               }
-
-                                               if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) {
-                                                       // Flush all saved tokens in case one of them is the bad one.
-                                                       Util.markTokensBad();
-                                                       button = new OO.ui.ButtonWidget( {
-                                                               label: mw.message( 'apisandbox-results-fixtoken' ).text()
-                                                       } );
-                                                       button.on( 'click', ApiSandbox.fixTokenAndResend )
-                                                               .on( 'click', button.setDisabled, [ true ], button )
-                                                               .$element.appendTo( $result );
-                                               }
-                                       }, function ( code, data ) {
-                                               var details = 'HTTP error: ' + data.exception;
-                                               $result.empty()
-                                                       .append(
-                                                               new OO.ui.LabelWidget( {
-                                                                       label: mw.message( 'apisandbox-results-error', details ).text(),
-                                                                       classes: [ 'error' ]
-                                                               } ).$element
-                                                       );
-                                       } );
-                       } );
-               },
-
-               /**
-                * Handler for the "Correct token and resubmit" button
-                *
-                * Used on a 'badtoken' error, it re-fetches token parameters for all
-                * pages and then re-submits the query.
-                */
-               fixTokenAndResend: function () {
-                       var page, subpages, i, k,
-                               ok = true,
-                               tokenWait = { dummy: true },
-                               checkPages = [ pages.main ],
-                               success = function ( k ) {
-                                       delete tokenWait[ k ];
-                                       if ( ok && $.isEmptyObject( tokenWait ) ) {
-                                               ApiSandbox.sendRequest();
-                                       }
-                               },
-                               failure = function ( k ) {
-                                       delete tokenWait[ k ];
-                                       ok = false;
-                               };
-
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-
-                               if ( page.tokenWidget ) {
-                                       k = page.apiModule + page.tokenWidget.paramInfo.name;
-                                       tokenWait[ k ] = page.tokenWidget.fetchToken();
-                                       tokenWait[ k ]
-                                               .done( success.bind( page.tokenWidget, k ) )
-                                               .fail( failure.bind( page.tokenWidget, k ) );
-                               }
-
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-
-                       success( 'dummy', '' );
-               },
-
-               /**
-                * Reset validity indicators for all widgets
-                */
-               updateValidityIndicators: function () {
-                       var page, subpages, i,
-                               checkPages = [ pages.main ];
-
-                       while ( checkPages.length ) {
-                               page = checkPages.shift();
-                               page.apiCheckValid();
-                               subpages = page.getSubpages();
-                               for ( i = 0; i < subpages.length; i++ ) {
-                                       if ( pages.hasOwnProperty( subpages[ i ].key ) ) {
-                                               checkPages.push( pages[ subpages[ i ].key ] );
-                                       }
-                               }
-                       }
-               }
-       };
-
-       /**
-        * PageLayout for API modules
-        *
-        * @class
-        * @private
-        * @extends OO.ui.PageLayout
-        * @constructor
-        * @param {Object} [config] Configuration options
-        */
-       ApiSandbox.PageLayout = function ( config ) {
-               config = $.extend( { prefix: '', expanded: false }, config );
-               this.displayText = config.key;
-               this.apiModule = config.path;
-               this.prefix = config.prefix;
-               this.paramInfo = null;
-               this.apiIsValid = true;
-               this.loadFromQueryParams = null;
-               this.widgets = {};
-               this.tokenWidget = null;
-               this.indentLevel = config.indentLevel ? config.indentLevel : 0;
-               ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config );
-               this.loadParamInfo();
-       };
-       OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout );
-       ApiSandbox.PageLayout.prototype.setupOutlineItem = function () {
-               this.outlineItem.setLevel( this.indentLevel );
-               this.outlineItem.setLabel( this.displayText );
-               this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' );
-               this.outlineItem.setIconTitle(
-                       this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
-               );
-       };
-
-       /**
-        * Fetch module information for this page's module, then create UI
-        */
-       ApiSandbox.PageLayout.prototype.loadParamInfo = function () {
-               var dynamicFieldset, dynamicParamNameWidget,
-                       that = this,
-                       removeDynamicParamWidget = function ( name, layout ) {
-                               dynamicFieldset.removeItems( [ layout ] );
-                               delete that.widgets[ name ];
-                       },
-                       addDynamicParamWidget = function () {
-                               var name, layout, widget, button;
-
-                               // Check name is filled in
-                               name = dynamicParamNameWidget.getValue().trim();
-                               if ( name === '' ) {
-                                       dynamicParamNameWidget.focus();
-                                       return;
-                               }
-
-                               if ( that.widgets[ name ] !== undefined ) {
-                                       windowManager.openWindow( 'errorAlert', {
-                                               title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ),
-                                               actions: [
-                                                       {
-                                                               action: 'accept',
-                                                               label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
-                                                               flags: 'primary'
-                                                       }
-                                               ]
-                                       } );
-                                       return;
-                               }
-
-                               widget = Util.createWidgetForParameter( {
-                                       name: name,
-                                       type: 'string',
-                                       'default': ''
-                               }, {
-                                       nooptional: true
-                               } );
-                               button = new OO.ui.ButtonWidget( {
-                                       icon: 'trash',
-                                       flags: 'destructive'
-                               } );
-                               layout = new OO.ui.ActionFieldLayout(
-                                       widget,
-                                       button,
-                                       {
-                                               label: name,
-                                               align: 'left'
-                                       }
-                               );
-                               button.on( 'click', removeDynamicParamWidget, [ name, layout ] );
-                               that.widgets[ name ] = widget;
-                               dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 );
-                               widget.focus();
-
-                               dynamicParamNameWidget.setValue( '' );
-                       };
-
-               this.$element.empty()
-                       .append( new OO.ui.ProgressBarWidget( {
-                               progress: false,
-                               text: mw.message( 'apisandbox-loading', this.displayText ).text()
-                       } ).$element );
-
-               Util.fetchModuleInfo( this.apiModule )
-                       .done( function ( pi ) {
-                               var prefix, i, j, descriptionContainer, widget, layoutConfig, button, widgetField, helpField, tmp, flag, count,
-                                       items = [],
-                                       deprecatedItems = [],
-                                       buttons = [],
-                                       filterFmModules = function ( v ) {
-                                               return v.substr( -2 ) !== 'fm' ||
-                                                       !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) );
-                                       },
-                                       widgetLabelOnClick = function () {
-                                               var f = this.getField();
-                                               if ( $.isFunction( f.setDisabled ) ) {
-                                                       f.setDisabled( false );
-                                               }
-                                               if ( $.isFunction( f.focus ) ) {
-                                                       f.focus();
-                                               }
-                                       };
-
-                               // This is something of a hack. We always want the 'format' and
-                               // 'action' parameters from the main module to be specified,
-                               // and for 'format' we also want to simplify the dropdown since
-                               // we always send the 'fm' variant.
-                               if ( that.apiModule === 'main' ) {
-                                       for ( i = 0; i < pi.parameters.length; i++ ) {
-                                               if ( pi.parameters[ i ].name === 'action' ) {
-                                                       pi.parameters[ i ].required = true;
-                                                       delete pi.parameters[ i ][ 'default' ];
-                                               }
-                                               if ( pi.parameters[ i ].name === 'format' ) {
-                                                       tmp = pi.parameters[ i ].type;
-                                                       for ( j = 0; j < tmp.length; j++ ) {
-                                                               availableFormats[ tmp[ j ] ] = true;
-                                                       }
-                                                       pi.parameters[ i ].type = tmp.filter( filterFmModules );
-                                                       pi.parameters[ i ][ 'default' ] = 'json';
-                                                       pi.parameters[ i ].required = true;
-                                               }
-                                       }
-                               }
-
-                               // Hide the 'wrappedhtml' parameter on format modules
-                               if ( pi.group === 'format' ) {
-                                       pi.parameters = pi.parameters.filter( function ( p ) {
-                                               return p.name !== 'wrappedhtml';
-                                       } );
-                               }
-
-                               that.paramInfo = pi;
-
-                               items.push( new OO.ui.FieldLayout(
-                                       new OO.ui.Widget( {} ).toggle( false ), {
-                                               align: 'top',
-                                               label: Util.parseHTML( pi.description )
-                                       }
-                               ) );
-
-                               if ( pi.helpurls.length ) {
-                                       buttons.push( new OO.ui.PopupButtonWidget( {
-                                               $overlay: true,
-                                               label: mw.message( 'apisandbox-helpurls' ).text(),
-                                               icon: 'help',
-                                               popup: {
-                                                       width: 'auto',
-                                                       padded: true,
-                                                       $content: $( '<ul>' ).append( pi.helpurls.map( function ( link ) {
-                                                               return $( '<li>' ).append( $( '<a>' )
-                                                                       .attr( { href: link, target: '_blank' } )
-                                                                       .text( link )
-                                                               );
-                                                       } ) )
-                                               }
-                                       } ) );
-                               }
-
-                               if ( pi.examples.length ) {
-                                       buttons.push( new OO.ui.PopupButtonWidget( {
-                                               $overlay: true,
-                                               label: mw.message( 'apisandbox-examples' ).text(),
-                                               icon: 'code',
-                                               popup: {
-                                                       width: 'auto',
-                                                       padded: true,
-                                                       $content: $( '<ul>' ).append( pi.examples.map( function ( example ) {
-                                                               var a = $( '<a>' )
-                                                                       .attr( 'href', '#' + example.query )
-                                                                       .html( example.description );
-                                                               a.find( 'a' ).contents().unwrap(); // Can't nest links
-                                                               return $( '<li>' ).append( a );
-                                                       } ) )
-                                               }
-                                       } ) );
-                               }
-
-                               if ( buttons.length ) {
-                                       items.push( new OO.ui.FieldLayout(
-                                               new OO.ui.ButtonGroupWidget( {
-                                                       items: buttons
-                                               } ), { align: 'top' }
-                                       ) );
-                               }
-
-                               if ( pi.parameters.length ) {
-                                       prefix = that.prefix + pi.prefix;
-                                       for ( i = 0; i < pi.parameters.length; i++ ) {
-                                               widget = Util.createWidgetForParameter( pi.parameters[ i ] );
-                                               that.widgets[ prefix + pi.parameters[ i ].name ] = widget;
-                                               if ( pi.parameters[ i ].tokentype ) {
-                                                       that.tokenWidget = widget;
-                                               }
-
-                                               descriptionContainer = $( '<div>' );
-
-                                               tmp = Util.parseHTML( pi.parameters[ i ].description );
-                                               tmp.filter( 'dl' ).makeCollapsible( {
-                                                       collapsed: true
-                                               } ).children( '.mw-collapsible-toggle' ).each( function () {
-                                                       var $this = $( this );
-                                                       $this.parent().prev( 'p' ).append( $this );
-                                               } );
-                                               descriptionContainer.append( $( '<div>' ).addClass( 'description' ).append( tmp ) );
-
-                                               if ( pi.parameters[ i ].info && pi.parameters[ i ].info.length ) {
-                                                       for ( j = 0; j < pi.parameters[ i ].info.length; j++ ) {
-                                                               descriptionContainer.append( $( '<div>' )
-                                                                       .addClass( 'info' )
-                                                                       .append( Util.parseHTML( pi.parameters[ i ].info[ j ] ) )
-                                                               );
-                                                       }
-                                               }
-                                               flag = true;
-                                               count = 1e100;
-                                               switch ( pi.parameters[ i ].type ) {
-                                                       case 'namespace':
-                                                               flag = false;
-                                                               count = mw.config.get( 'wgFormattedNamespaces' ).length;
-                                                               break;
-
-                                                       case 'limit':
-                                                               if ( pi.parameters[ i ].highmax !== undefined ) {
-                                                                       descriptionContainer.append( $( '<div>' )
-                                                                               .addClass( 'info' )
-                                                                               .append(
-                                                                                       Util.parseMsg(
-                                                                                               'api-help-param-limit2', pi.parameters[ i ].max, pi.parameters[ i ].highmax
-                                                                                       ),
-                                                                                       ' ',
-                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
-                                                                               )
-                                                                       );
-                                                               } else {
-                                                                       descriptionContainer.append( $( '<div>' )
-                                                                               .addClass( 'info' )
-                                                                               .append(
-                                                                                       Util.parseMsg( 'api-help-param-limit', pi.parameters[ i ].max ),
-                                                                                       ' ',
-                                                                                       Util.parseMsg( 'apisandbox-param-limit' )
-                                                                               )
-                                                                       );
-                                                               }
-                                                               break;
-
-                                                       case 'integer':
-                                                               tmp = '';
-                                                               if ( pi.parameters[ i ].min !== undefined ) {
-                                                                       tmp += 'min';
-                                                               }
-                                                               if ( pi.parameters[ i ].max !== undefined ) {
-                                                                       tmp += 'max';
-                                                               }
-                                                               if ( tmp !== '' ) {
-                                                                       descriptionContainer.append( $( '<div>' )
-                                                                               .addClass( 'info' )
-                                                                               .append( Util.parseMsg(
-                                                                                       'api-help-param-integer-' + tmp,
-                                                                                       Util.apiBool( pi.parameters[ i ].multi ) ? 2 : 1,
-                                                                                       pi.parameters[ i ].min, pi.parameters[ i ].max
-                                                                               ) )
-                                                                       );
-                                                               }
-                                                               break;
-
-                                                       default:
-                                                               if ( Array.isArray( pi.parameters[ i ].type ) ) {
-                                                                       flag = false;
-                                                                       count = pi.parameters[ i ].type.length;
-                                                               }
-                                                               break;
-                                               }
-                                               if ( Util.apiBool( pi.parameters[ i ].multi ) ) {
-                                                       tmp = [];
-                                                       if ( flag && !( widget instanceof OO.ui.TagMultiselectWidget ) &&
-                                                               !(
-                                                                       widget instanceof OptionalWidget &&
-                                                                       widget.widget instanceof OO.ui.TagMultiselectWidget
-                                                               )
-                                                       ) {
-                                                               tmp.push( mw.message( 'api-help-param-multi-separate' ).parse() );
-                                                       }
-                                                       if ( count > pi.parameters[ i ].lowlimit ) {
-                                                               tmp.push(
-                                                                       mw.message( 'api-help-param-multi-max',
-                                                                               pi.parameters[ i ].lowlimit, pi.parameters[ i ].highlimit
-                                                                       ).parse()
-                                                               );
-                                                       }
-                                                       if ( tmp.length ) {
-                                                               descriptionContainer.append( $( '<div>' )
-                                                                       .addClass( 'info' )
-                                                                       .append( Util.parseHTML( tmp.join( ' ' ) ) )
-                                                               );
-                                                       }
-                                               }
-                                               if ( 'maxbytes' in pi.parameters[ i ] ) {
-                                                       descriptionContainer.append( $( '<div>' )
-                                                               .addClass( 'info' )
-                                                               .append( Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes ) )
-                                                       );
-                                               }
-                                               if ( 'maxchars' in pi.parameters[ i ] ) {
-                                                       descriptionContainer.append( $( '<div>' )
-                                                               .addClass( 'info' )
-                                                               .append( Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars ) )
-                                                       );
-                                               }
-                                               helpField = new OO.ui.FieldLayout(
-                                                       new OO.ui.Widget( {
-                                                               $content: '\xa0',
-                                                               classes: [ 'mw-apisandbox-spacer' ]
-                                                       } ), {
-                                                               align: 'inline',
-                                                               classes: [ 'mw-apisandbox-help-field' ],
-                                                               label: descriptionContainer
-                                                       }
-                                               );
-
-                                               layoutConfig = {
-                                                       align: 'left',
-                                                       classes: [ 'mw-apisandbox-widget-field' ],
-                                                       label: prefix + pi.parameters[ i ].name
-                                               };
-
-                                               if ( pi.parameters[ i ].tokentype ) {
-                                                       button = new OO.ui.ButtonWidget( {
-                                                               label: mw.message( 'apisandbox-fetch-token' ).text()
-                                                       } );
-                                                       button.on( 'click', widget.fetchToken, [], widget );
-
-                                                       widgetField = new OO.ui.ActionFieldLayout( widget, button, layoutConfig );
-                                               } else {
-                                                       widgetField = new OO.ui.FieldLayout( widget, layoutConfig );
-                                               }
-
-                                               // We need our own click handler on the widget label to
-                                               // turn off the disablement.
-                                               widgetField.$label.on( 'click', widgetLabelOnClick.bind( widgetField ) );
-
-                                               // Don't grey out the label when the field is disabled,
-                                               // it makes it too hard to read and our "disabled"
-                                               // isn't really disabled.
-                                               widgetField.onFieldDisable( false );
-                                               widgetField.onFieldDisable = $.noop;
-
-                                               if ( Util.apiBool( pi.parameters[ i ].deprecated ) ) {
-                                                       deprecatedItems.push( widgetField, helpField );
-                                               } else {
-                                                       items.push( widgetField, helpField );
-                                               }
-                                       }
-                               }
-
-                               if ( !pi.parameters.length && !Util.apiBool( pi.dynamicparameters ) ) {
-                                       items.push( new OO.ui.FieldLayout(
-                                               new OO.ui.Widget( {} ).toggle( false ), {
-                                                       align: 'top',
-                                                       label: Util.parseMsg( 'apisandbox-no-parameters' )
-                                               }
-                                       ) );
-                               }
-
-                               that.$element.empty();
-
-                               new OO.ui.FieldsetLayout( {
-                                       label: that.displayText
-                               } ).addItems( items )
-                                       .$element.appendTo( that.$element );
-
-                               if ( Util.apiBool( pi.dynamicparameters ) ) {
-                                       dynamicFieldset = new OO.ui.FieldsetLayout();
-                                       dynamicParamNameWidget = new OO.ui.TextInputWidget( {
-                                               placeholder: mw.message( 'apisandbox-dynamic-parameters-add-placeholder' ).text()
-                                       } ).on( 'enter', addDynamicParamWidget );
-                                       dynamicFieldset.addItems( [
-                                               new OO.ui.FieldLayout(
-                                                       new OO.ui.Widget( {} ).toggle( false ), {
-                                                               align: 'top',
-                                                               label: Util.parseHTML( pi.dynamicparameters )
-                                                       }
-                                               ),
-                                               new OO.ui.ActionFieldLayout(
-                                                       dynamicParamNameWidget,
-                                                       new OO.ui.ButtonWidget( {
-                                                               icon: 'add',
-                                                               flags: 'progressive'
-                                                       } ).on( 'click', addDynamicParamWidget ),
-                                                       {
-                                                               label: mw.message( 'apisandbox-dynamic-parameters-add-label' ).text(),
-                                                               align: 'left'
-                                                       }
-                                               )
-                                       ] );
-                                       $( '<fieldset>' )
-                                               .append(
-                                                       $( '<legend>' ).text( mw.message( 'apisandbox-dynamic-parameters' ).text() ),
-                                                       dynamicFieldset.$element
-                                               )
-                                               .appendTo( that.$element );
-                               }
-
-                               if ( deprecatedItems.length ) {
-                                       tmp = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
-                                       $( '<fieldset>' )
-                                               .append(
-                                                       $( '<legend>' ).append(
-                                                               new OO.ui.ToggleButtonWidget( {
-                                                                       label: mw.message( 'apisandbox-deprecated-parameters' ).text()
-                                                               } ).on( 'change', tmp.toggle, [], tmp ).$element
-                                                       ),
-                                                       tmp.$element
-                                               )
-                                               .appendTo( that.$element );
-                               }
-
-                               // Load stored params, if any, then update the booklet if we
-                               // have subpages (or else just update our valid-indicator).
-                               tmp = that.loadFromQueryParams;
-                               that.loadFromQueryParams = null;
-                               if ( $.isPlainObject( tmp ) ) {
-                                       that.loadQueryParams( tmp );
-                               }
-                               if ( that.getSubpages().length > 0 ) {
-                                       ApiSandbox.updateUI( tmp );
-                               } else {
-                                       that.apiCheckValid();
-                               }
-                       } ).fail( function ( code, detail ) {
-                               that.$element.empty()
-                                       .append(
-                                               new OO.ui.LabelWidget( {
-                                                       label: mw.message( 'apisandbox-load-error', that.apiModule, detail ).text(),
-                                                       classes: [ 'error' ]
-                                               } ).$element,
-                                               new OO.ui.ButtonWidget( {
-                                                       label: mw.message( 'apisandbox-retry' ).text()
-                                               } ).on( 'click', that.loadParamInfo, [], that ).$element
-                                       );
-                       } );
-       };
-
-       /**
-        * Check that all widgets on the page are in a valid state.
-        *
-        * @return {jQuery.Promise[]} One promise for each widget, resolved with `false` if invalid
-        */
-       ApiSandbox.PageLayout.prototype.apiCheckValid = function () {
-               var promises, that = this;
-
-               if ( this.paramInfo === null ) {
-                       return [];
-               } else {
-                       promises = $.map( this.widgets, function ( widget ) {
-                               return widget.apiCheckValid();
-                       } );
-                       $.when.apply( $, promises ).then( function () {
-                               that.apiIsValid = $.inArray( false, arguments ) === -1;
-                               if ( that.getOutlineItem() ) {
-                                       that.getOutlineItem().setIcon( that.apiIsValid || suppressErrors ? null : 'alert' );
-                                       that.getOutlineItem().setIconTitle(
-                                               that.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain()
-                                       );
-                               }
-                       } );
-                       return promises;
-               }
-       };
-
-       /**
-        * Load form fields from query parameters
-        *
-        * @param {Object} params
-        */
-       ApiSandbox.PageLayout.prototype.loadQueryParams = function ( params ) {
-               if ( this.paramInfo === null ) {
-                       this.loadFromQueryParams = params;
-               } else {
-                       $.each( this.widgets, function ( name, widget ) {
-                               var v = params.hasOwnProperty( name ) ? params[ name ] : undefined;
-                               widget.setApiValue( v );
-                       } );
-               }
-       };
-
-       /**
-        * Load query params from form fields
-        *
-        * @param {Object} params Write query parameters into this object
-        * @param {Object} displayParams Write query parameters for display into this object
-        */
-       ApiSandbox.PageLayout.prototype.getQueryParams = function ( params, displayParams ) {
-               $.each( this.widgets, function ( name, widget ) {
-                       var value = widget.getApiValue();
-                       if ( value !== undefined ) {
-                               params[ name ] = value;
-                               if ( $.isFunction( widget.getApiValueForDisplay ) ) {
-                                       value = widget.getApiValueForDisplay();
-                               }
-                               displayParams[ name ] = value;
-                       }
-               } );
-       };
-
-       /**
-        * Fetch a list of subpage names loaded by this page
-        *
-        * @return {Array}
-        */
-       ApiSandbox.PageLayout.prototype.getSubpages = function () {
-               var ret = [];
-               $.each( this.widgets, function ( name, widget ) {
-                       var submodules, i;
-                       if ( $.isFunction( widget.getSubmodules ) ) {
-                               submodules = widget.getSubmodules();
-                               for ( i = 0; i < submodules.length; i++ ) {
-                                       ret.push( {
-                                               key: name + '=' + submodules[ i ].value,
-                                               path: submodules[ i ].path,
-                                               prefix: widget.paramInfo.submoduleparamprefix || ''
-                                       } );
-                               }
-                       }
-               } );
-               return ret;
-       };
-
-       $( ApiSandbox.init );
-
-       module.exports = ApiSandbox;
-
-}( jQuery, mediaWiki, OO ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css b/resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css
deleted file mode 100644 (file)
index 4dc4c27..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-.client-js .mw-apisandbox-nojs {
-       display: none;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.block.js b/resources/src/mediawiki.special/mediawiki.special.block.js
deleted file mode 100644 (file)
index 180f040..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*!
- * JavaScript for Special:Block
- */
-( function ( mw, $ ) {
-       // Like OO.ui.infuse(), but if the element doesn't exist, return null instead of throwing an exception.
-       function infuseOrNull( elem ) {
-               try {
-                       return OO.ui.infuse( elem );
-               } catch ( er ) {
-                       return null;
-               }
-       }
-
-       $( function () {
-               // This code is also loaded on the "block succeeded" page where there is no form,
-               // so username and expiry fields might also be missing.
-               var blockTargetWidget = infuseOrNull( 'mw-bi-target' ),
-                       anonOnlyField = infuseOrNull( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
-                       enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
-                       hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
-                       watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
-                       expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
-
-               function updateBlockOptions() {
-                       var blocktarget = blockTargetWidget.getValue().trim(),
-                               isEmpty = blocktarget === '',
-                               isIp = mw.util.isIPAddress( blocktarget, true ),
-                               isIpRange = isIp && blocktarget.match( /\/\d+$/ ),
-                               isNonEmptyIp = isIp && !isEmpty,
-                               expiryValue = expiryWidget.getValue(),
-                               // infinityValues  are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
-                               infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
-                               isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
-
-                       if ( enableAutoblockField ) {
-                               enableAutoblockField.toggle( !( isNonEmptyIp ) );
-                       }
-                       if ( hideUserField ) {
-                               hideUserField.toggle( !( isNonEmptyIp || !isIndefinite ) );
-                       }
-                       if ( anonOnlyField ) {
-                               anonOnlyField.toggle( !( !isIp && !isEmpty ) );
-                       }
-                       if ( watchUserField ) {
-                               watchUserField.toggle( !( isIpRange && !isEmpty ) );
-                       }
-               }
-
-               if ( blockTargetWidget ) {
-                       // Bind functions so they're checked whenever stuff changes
-                       blockTargetWidget.on( 'change', updateBlockOptions );
-                       expiryWidget.on( 'change', updateBlockOptions );
-
-                       // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
-                       updateBlockOptions();
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changecredentials.js b/resources/src/mediawiki.special/mediawiki.special.changecredentials.js
deleted file mode 100644 (file)
index ad8a4f4..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*!
- * JavaScript for change credentials form.
- */
-( function ( mw, $, OO ) {
-       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               var api = new mw.Api();
-
-               $root.find( '.mw-changecredentials-validate-password.oo-ui-fieldLayout' ).each( function () {
-                       var currentApiPromise,
-                               self = OO.ui.FieldLayout.static.infuse( $( this ) );
-
-                       self.getField().setValidation( function ( password ) {
-                               var d;
-
-                               if ( currentApiPromise ) {
-                                       currentApiPromise.abort();
-                                       currentApiPromise = undefined;
-                               }
-
-                               password = password.trim();
-
-                               if ( password === '' ) {
-                                       self.setErrors( [] );
-                                       return true;
-                               }
-
-                               d = $.Deferred();
-                               currentApiPromise = api.post( {
-                                       action: 'validatepassword',
-                                       password: password,
-                                       formatversion: 2,
-                                       errorformat: 'html',
-                                       errorsuselocal: true,
-                                       uselang: mw.config.get( 'wgUserLanguage' )
-                               } ).done( function ( resp ) {
-                                       var pwinfo = resp.validatepassword,
-                                               good = pwinfo.validity === 'Good',
-                                               errors = [];
-
-                                       currentApiPromise = undefined;
-
-                                       if ( !good ) {
-                                               pwinfo.validitymessages.map( function ( m ) {
-                                                       errors.push( new OO.ui.HtmlSnippet( m.html ) );
-                                               } );
-                                       }
-                                       self.setErrors( errors );
-                                       d.resolve( good );
-                               } ).fail( d.reject );
-
-                               return d.promise( { abort: currentApiPromise.abort } );
-                       } );
-               } );
-       } );
-}( mediaWiki, jQuery, OO ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.css
deleted file mode 100644 (file)
index 65860ea..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*!
- * Styling for Special:Watchlist and Special:RecentChanges
- */
-
-.mw-changeslist-line-watched .mw-title {
-       font-weight: bold;
-}
-
-/*
- * Titles, including username links, and also tag names
- * are prone to getting jumbled up
- * with other titles, usernames, etc. in mixed RTL-LTR environment.
- */
-.mw-changeslist .mw-tag-marker,
-.mw-changeslist .mw-title {
-       unicode-bidi: embed;
-}
-
-/* Colored watchlist and recent changes numbers */
-.mw-plusminus-pos {
-       color: #006400; /* dark green */
-}
-
-.mw-plusminus-neg {
-       color: #8b0000; /* dark red */
-}
-
-.mw-plusminus-null {
-       color: #a2a9b1; /* gray */
-}
-
-/*
- * Bidi-isolate these numbers.
- * See https://phabricator.wikimedia.org/T93484
- */
-.mw-plusminus-pos,
-.mw-plusminus-neg,
-.mw-plusminus-null {
-       unicode-bidi: -moz-isolate;
-       unicode-bidi: isolate;
-}
-
-/* Prevent FOUC if legend is initially collapsed */
-.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
-       display: none;
-}
-
-.mw-changeslist-legend.mw-collapsed {
-       margin-bottom: 0;
-}
-
-/* Prevent pushing down the content if legend is collapsed */
-.mw-changeslist-legend.mw-collapsed ~ ul:first-of-type > li:first-child,
-.mw-changeslist-legend.mw-collapsed + h4 + div > table.mw-changeslist-line:first-child {
-       clear: right;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.enhanced.css
deleted file mode 100644 (file)
index cb11332..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*!
- * Styling for Special:Watchlist and Special:RecentChanges when preference 'usenewrc'
- * a.k.a. Enhanced Recent Changes is enabled.
- */
-
-table.mw-enhanced-rc {
-       border: 0;
-       border-spacing: 0;
-}
-
-table.mw-enhanced-rc th,
-table.mw-enhanced-rc td {
-       padding: 0;
-       vertical-align: top;
-}
-
-td.mw-enhanced-rc {
-       white-space: nowrap;
-       font-family: monospace, monospace;
-}
-
-.mw-enhanced-rc-time {
-       font-family: monospace, monospace;
-}
-
-table.mw-enhanced-rc td.mw-enhanced-rc-nested {
-       padding-left: 1em;
-}
-
-/* Show/hide arrows in enhanced changeslist */
-.mw-enhanced-rc .collapsible-expander {
-       float: none;
-}
-
-/* If JS is disabled, the arrows or the placeholder space shouldn't be shown */
-.client-nojs .mw-enhancedchanges-arrow-space {
-       display: none;
-}
-
-/*
- * And if it's enabled, let's optimize the collapsing a little: hide the rows
- * that would be hidden by jquery.makeCollapsible with CSS to save us some
- * reflows and repaints. This doesn't work on browsers that don't fully support
- * CSS2 (IE6), but it's okay, this will be done in JavaScript with old degraded
- * performance instead.
- */
-.client-js table.mw-enhanced-rc.mw-collapsed tr + tr {
-       display: none;
-}
-
-.mw-enhancedchanges-arrow {
-       padding-top: 2px;
-}
-
-.mw-enhancedchanges-arrow-space {
-       display: inline-block;
-       *display: inline; /* IE7 and below */
-       zoom: 1;
-       width: 15px;
-       height: 15px;
-}
-
-.mw-enhanced-watched .mw-enhanced-rc-time {
-       font-weight: bold;
-}
-
-span.changedby {
-       font-size: 95%;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.css
deleted file mode 100644 (file)
index 14f6aee..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*!
- * Styling for changes list legend
- */
-
-.mw-changeslist-legend {
-       float: right;
-       margin-left: 1em;
-       margin-bottom: 0.5em;
-       clear: right;
-       font-size: 85%;
-       line-height: 1.2em;
-       padding: 0.5em;
-       border: 1px solid #ddd;
-}
-
-.mw-changeslist-legend dl {
-       /* Parent element defines sufficient padding */
-       margin-bottom: 0;
-}
-
-.mw-changeslist-legend dt {
-       float: left;
-       margin: 0 0.5em 0 0;
-}
-
-.mw-changeslist-legend dd {
-       margin-left: 1.5em;
-}
-
-.mw-changeslist-legend dt,
-.mw-changeslist-legend dd {
-       line-height: 1.3em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js b/resources/src/mediawiki.special/mediawiki.special.changeslist.legend.js
deleted file mode 100644 (file)
index 0792762..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*!
- * Script for changes list legend
- */
-
-/* Remember the collapse state of the legend on recent changes and watchlist pages. */
-( function ( mw ) {
-       var
-               cookieName = 'changeslist-state',
-               // Expanded by default
-               doCollapsibleLegend = function ( $container ) {
-                       $container.find( '.mw-changeslist-legend' )
-                               .makeCollapsible( {
-                                       collapsed: mw.cookie.get( cookieName ) === 'collapsed'
-                               } )
-                               .on( 'beforeExpand.mw-collapsible', function () {
-                                       mw.cookie.set( cookieName, 'expanded' );
-                               } )
-                               .on( 'beforeCollapse.mw-collapsible', function () {
-                                       mw.cookie.set( cookieName, 'collapsed' );
-                               } );
-               };
-
-       mw.hook( 'wikipage.content' ).add( doCollapsibleLegend );
-}( mediaWiki ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js b/resources/src/mediawiki.special/mediawiki.special.changeslist.visitedstatus.js
deleted file mode 100644 (file)
index 6b25327..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-/*!
- * JavaScript for Special:Watchlist
- */
-( function ( $ ) {
-       $( function () {
-               $( '.mw-changeslist-line-watched .mw-title a' ).on( 'click', function () {
-                       $( this )
-                               .closest( '.mw-changeslist-line-watched' )
-                               .removeClass( 'mw-changeslist-line-watched' );
-               } );
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less b/resources/src/mediawiki.special/mediawiki.special.comparepages.styles.less
deleted file mode 100644 (file)
index 87b7a8b..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-@import 'mediawiki.mixins';
-
-.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
-       width: 100%;
-}
-
-.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
-       float: left;
-       width: 49%;
-       .box-sizing( border-box );
-}
-
-.mw-special-ComparePages .oo-ui-layout.oo-ui-panelLayout.oo-ui-panelLayout-padded.oo-ui-panelLayout-framed:nth-of-type( 2 ) {
-       margin-left: 2%;
-}
-
-.mw-special-ComparePages .mw-htmlform-submit-buttons {
-       clear: both;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.contributions.js b/resources/src/mediawiki.special/mediawiki.special.contributions.js
deleted file mode 100644 (file)
index f65a257..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-( function ( mw, $ ) {
-       $( function () {
-               var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
-                       endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
-
-               startInput.on( 'deactivate', function ( userSelected ) {
-                       if ( userSelected ) {
-                               endInput.focus();
-                       }
-               } );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.css b/resources/src/mediawiki.special/mediawiki.special.edittags.css
deleted file mode 100644 (file)
index 204009c..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*!
- * Styling for Special:EditTags and action=editchangetags
- */
-#mw-edittags-tags-selector td {
-       vertical-align: top;
-}
-
-#mw-edittags-tags-selector-multi td {
-       vertical-align: top;
-       padding-right: 1.5em;
-}
-
-#mw-edittags-tag-list {
-       min-width: 20em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.edittags.js b/resources/src/mediawiki.special/mediawiki.special.edittags.js
deleted file mode 100644 (file)
index 4f51e9b..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*!
- * JavaScript for Special:EditTags
- */
-( function ( mw, $ ) {
-       $( function () {
-               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-                       $wpReason = $( '#wpReason' ),
-                       $tagList = $( '#mw-edittags-tag-list' );
-
-               if ( $tagList.length ) {
-                       $tagList.chosen( {
-                               /* eslint-disable camelcase */
-                               placeholder_text_multiple: mw.msg( 'tags-edit-chosen-placeholder' ),
-                               no_results_text: mw.msg( 'tags-edit-chosen-no-results' )
-                               /* eslint-enable camelcase */
-                       } );
-               }
-
-               $( '#mw-edittags-remove-all' ).on( 'change', function ( e ) {
-                       $( '.mw-edittags-remove-checkbox' ).prop( 'checked', e.target.checked );
-               } );
-               $( '.mw-edittags-remove-checkbox' ).on( 'change', function ( e ) {
-                       if ( !e.target.checked ) {
-                               $( '#mw-edittags-remove-all' ).prop( 'checked', false );
-                       }
-               } );
-
-               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-               // use maxLength because it's leaving room for log entry text.
-               if ( summaryCodePointLimit ) {
-                       $wpReason.codePointLimit();
-               } else if ( summaryByteLimit ) {
-                       $wpReason.byteLimit();
-               }
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.import.js b/resources/src/mediawiki.special/mediawiki.special.import.js
deleted file mode 100644 (file)
index 2cb96af..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*!
- * JavaScript for Special:Import
- */
-( function ( $ ) {
-       var subprojectListAlreadyShown;
-       function updateImportSubprojectList() {
-               var $projectField = $( '#mw-import-table-interwiki #interwiki' ),
-                       $subprojectField = $projectField.parent().find( '#subproject' ),
-                       $selected = $projectField.find( ':selected' ),
-                       oldValue = $subprojectField.val(),
-                       option, options;
-
-               if ( $selected.attr( 'data-subprojects' ) ) {
-                       options = $selected.attr( 'data-subprojects' ).split( ' ' ).map( function ( el ) {
-                               option = document.createElement( 'option' );
-                               option.appendChild( document.createTextNode( el ) );
-                               option.setAttribute( 'value', el );
-                               if ( oldValue === el && subprojectListAlreadyShown === true ) {
-                                       option.setAttribute( 'selected', 'selected' );
-                               }
-                               return option;
-                       } );
-                       $subprojectField.show().empty().append( options );
-                       subprojectListAlreadyShown = true;
-               } else {
-                       $subprojectField.hide();
-               }
-       }
-
-       $( function () {
-               var $projectField = $( '#mw-import-table-interwiki #interwiki' );
-               if ( $projectField.length ) {
-                       $projectField.change( updateImportSubprojectList );
-                       updateImportSubprojectList();
-               }
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.css b/resources/src/mediawiki.special/mediawiki.special.movePage.css
deleted file mode 100644 (file)
index 9428fed..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * Styles for Special:MovePage
- */
-
-.movepage-wrapper {
-       width: 50em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.movePage.js b/resources/src/mediawiki.special/mediawiki.special.movePage.js
deleted file mode 100644 (file)
index d828396..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*!
- * JavaScript for Special:MovePage
- */
-( function ( mw, $ ) {
-       $( function () {
-               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-                       wpReason = OO.ui.infuse( $( '#wpReason' ) );
-
-               // Infuse for pretty dropdown
-               OO.ui.infuse( $( '#wpNewTitle' ) );
-               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-               if ( summaryCodePointLimit ) {
-                       mw.widgets.visibleCodePointLimit( wpReason, summaryCodePointLimit );
-               } else if ( summaryByteLimit ) {
-                       mw.widgets.visibleByteLimit( wpReason, summaryByteLimit );
-               }
-               // Infuse for nicer "help" popup
-               if ( $( '#wpMovetalk-field' ).length ) {
-                       OO.ui.infuse( $( '#wpMovetalk-field' ) );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js b/resources/src/mediawiki.special/mediawiki.special.pageLanguage.js
deleted file mode 100644 (file)
index edfbe1e..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
- * JavaScript module used on Special:PageLanguage
- */
-( function ( $, OO ) {
-       $( function () {
-               // Select the 'Language select' option if user is trying to select language
-               OO.ui.infuse( 'mw-pl-languageselector' ).on( 'change', function () {
-                       OO.ui.infuse( 'mw-pl-options' ).setValue( '2' );
-               } );
-       } );
-}( jQuery, OO ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css b/resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css
deleted file mode 100644 (file)
index 7ef75d0..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-/* Distinguish actual data from information about it being hidden visually */
-.prop-value-hidden {
-       font-style: italic;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js b/resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
deleted file mode 100644 (file)
index 244154b..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
- * closed when any form field is changed.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var allowCloseWindow, saveButton, restoreButton,
-                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
-
-               // Check if all of the form values are unchanged.
-               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
-               // slower and more complicated. It works fine to treat them as HTML elements.)
-               function isPrefsChanged() {
-                       var inputs = $( '#mw-prefs-form :input[name]' ),
-                               input, $input, inputType,
-                               index, optIndex,
-                               opt;
-
-                       for ( index = 0; index < inputs.length; index++ ) {
-                               input = inputs[ index ];
-                               $input = $( input );
-
-                               // Different types of inputs have different methods for accessing defaults
-                               if ( $input.is( 'select' ) ) {
-                                       // <select> has the property defaultSelected for each option
-                                       for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
-                                               opt = input.options[ optIndex ];
-                                               if ( opt.selected !== opt.defaultSelected ) {
-                                                       return true;
-                                               }
-                                       }
-                               } else if ( $input.is( 'input' ) || $input.is( 'textarea' ) ) {
-                                       // <input> has defaultValue or defaultChecked
-                                       inputType = input.type;
-                                       if ( inputType === 'radio' || inputType === 'checkbox' ) {
-                                               if ( input.checked !== input.defaultChecked ) {
-                                                       return true;
-                                               }
-                                       } else if ( input.value !== input.defaultValue ) {
-                                               return true;
-                                       }
-                               }
-                       }
-
-                       return false;
-               }
-
-               if ( oouiEnabled ) {
-                       saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
-                       restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
-
-                       // Disable the button to save preferences unless preferences have changed
-                       // Check if preferences have been changed before JS has finished loading
-                       saveButton.setDisabled( !isPrefsChanged() );
-                       $( '#preferences .oo-ui-fieldsetLayout' ).on( 'change keyup mouseup', function () {
-                               saveButton.setDisabled( !isPrefsChanged() );
-                       } );
-               } else {
-                       // Disable the button to save preferences unless preferences have changed
-                       // Check if preferences have been changed before JS has finished loading
-                       $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
-                       $( '#preferences > fieldset' ).on( 'change keyup mouseup', function () {
-                               $( '#prefcontrol' ).prop( 'disabled', !isPrefsChanged() );
-                       } );
-               }
-
-               // Set up a message to notify users if they try to leave the page without
-               // saving.
-               allowCloseWindow = mw.confirmCloseWindow( {
-                       test: isPrefsChanged,
-                       message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
-                       namespace: 'prefswarning'
-               } );
-               $( '#mw-prefs-form' ).on( 'submit', $.proxy( allowCloseWindow, 'release' ) );
-               if ( oouiEnabled ) {
-                       restoreButton.on( 'click', function () {
-                               allowCloseWindow.release();
-                               // The default behavior of events in OOUI is always prevented. Follow the link manually.
-                               // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
-                               location.href = restoreButton.getHref();
-                       } );
-               } else {
-                       $( '#mw-prefs-restoreprefs' ).on( 'click', $.proxy( allowCloseWindow, 'release' ) );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js b/resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js
deleted file mode 100644 (file)
index e6b7432..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
- */
-( function ( $ ) {
-       $( function () {
-               var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
-               convertmessagebox();
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js b/resources/src/mediawiki.special/mediawiki.special.preferences.editfont.js
deleted file mode 100644 (file)
index fe48886..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: editfont field enhancements.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var widget, lastValue;
-
-               try {
-                       widget = OO.ui.infuse( $( '#mw-input-wpeditfont' ) );
-               } catch ( err ) {
-                       // This preference could theoretically be disabled ($wgHiddenPrefs)
-                       return;
-               }
-
-               // Style options
-               widget.dropdownWidget.menu.items.forEach( function ( item ) {
-                       item.$label.addClass( 'mw-editfont-' + item.getData() );
-               } );
-
-               function updateLabel( value ) {
-                       // Style selected item label
-                       widget.dropdownWidget.$label
-                               .removeClass( 'mw-editfont-' + lastValue )
-                               .addClass( 'mw-editfont-' + value );
-                       lastValue = value;
-               }
-
-               widget.on( 'change', updateLabel );
-               updateLabel( widget.getValue() );
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js b/resources/src/mediawiki.special/mediawiki.special.preferences.personalEmail.js
deleted file mode 100644 (file)
index f934d59..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Email preferences better UX
- */
-( function ( $ ) {
-       $( function () {
-               var allowEmail, allowEmailFromNewUsers;
-
-               allowEmail = $( '#wpAllowEmail' );
-               allowEmailFromNewUsers = $( '#wpAllowEmailFromNewUsers' );
-
-               function toggleDisabled() {
-                       if ( allowEmail.is( ':checked' ) && allowEmail.is( ':enabled' ) ) {
-                               allowEmailFromNewUsers.prop( 'disabled', false );
-                       } else {
-                               allowEmailFromNewUsers.prop( 'disabled', true );
-                       }
-               }
-
-               if ( allowEmail ) {
-                       allowEmail.on( 'change', toggleDisabled );
-                       toggleDisabled();
-               }
-       } );
-}( jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
deleted file mode 100644 (file)
index 8810318..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .oo-ui-labelWidget,
-.mw-email-none .oo-ui-labelWidget {
-       border: 1px solid #fde29b;
-       background-color: #fdf1d1;
-       color: #000;
-       padding: 0.5em;
-}
-/* Authenticated email field has its own class too. Unstyled by default */
-/*
-.mw-email-authenticated .oo-ui-labelWidget { }
-*/
-
-/* This is needed because add extra buttons in a weird way */
-.mw-prefs-buttons .mw-htmlform-submit-buttons {
-       margin: 0;
-       display: inline;
-}
-
-.mw-prefs-buttons {
-       margin-top: 1em;
-}
-
-#prefcontrol {
-       margin-right: 0.5em;
-}
-
-/*
- * Hide, but keep accessible for screen-readers.
- * Like .mw-jump, #jump-to-nav from shared.css
- */
-.client-js .mw-navigation-hint {
-       overflow: hidden;
-       height: 0;
-       zoom: 1;
-}
-
-/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
- * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
- * better solved by setting overlays for the widgets, but we can't do it from PHP... */
-#preferences .oo-ui-panelLayout {
-       position: static;
-       overflow: visible;
-       -webkit-transform: none;
-       transform: none;
-}
-
-#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
-       border-color: #c8ccd1;
-       border-width: 1px 0 0;
-       border-radius: 0;
-       padding-left: 0;
-       padding-right: 0;
-       box-shadow: none;
-}
-
-/* Tweak the margins to reduce the shifting of form contents
- * after JS code loads and rearranges the page */
-.client-js #preferences > .oo-ui-panelLayout {
-       margin: 1em 0;
-}
-
-.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
-       margin-left: 0.25em;
-}
-
-.client-js #preferences .oo-ui-tabPanelLayout {
-       padding-top: 0.5em;
-       padding-bottom: 0.5em;
-}
-
-.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
-       margin-left: 0;
-       margin-bottom: 0;
-       border: 0;
-       padding-top: 0;
-}
-
-.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
-       margin-bottom: 1em;
-}
-
-/* Make the "Basic information" section more compact */
-/* OOUI's `align: 'left'` for FieldLayouts sucks, so we do our own */
-#mw-htmlform-info > .oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
-       width: 20%;
-       display: inline-block;
-       vertical-align: middle;
-       padding: 0;
-}
-
-#mw-htmlform-info > .oo-ui-fieldLayout-align-top .oo-ui-fieldLayout-help {
-       margin-right: 0;
-}
-
-#mw-htmlform-info > .oo-ui-fieldLayout.oo-ui-fieldLayout-align-top > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-field {
-       width: 80%;
-       display: inline-block;
-       vertical-align: middle;
-}
-
-/* Expand the dropdown and textfield of "Time zone" field to the */
-/* usual maximum width and display them on separate lines. */
-#wpTimeCorrection .oo-ui-dropdownInputWidget,
-#wpTimeCorrection .oo-ui-textInputWidget {
-       display: block;
-       max-width: 50em;
-}
-
-#wpTimeCorrection .oo-ui-textInputWidget {
-       margin-top: 0.5em;
-}
-
-/* HACK: expand width of gadget descriptions.
- * This should be moved to the Gadgets extension */
-#mw-htmlform-gadgets .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body {
-       max-width: none;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css b/resources/src/mediawiki.special/mediawiki.special.preferences.styles.legacy.css
deleted file mode 100644 (file)
index 33b630a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
-       border: 1px solid #fde29b;
-       background-color: #fdf1d1;
-       color: #000;
-}
-/* Authenticated email field has its own class too. Unstyled by default */
-/*
-.mw-email-authenticated .mw-input { }
-*/
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
-       width: 20%;
-}
-
-#preferences > fieldset table {
-       width: 100%;
-}
-#preferences > fieldset table.mw-htmlform-matrix {
-       width: auto;
-}
-
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
-
-/*
- * Hide, but keep accessible for screen-readers.
- * Like .mw-jump, #jump-to-nav from shared.css
- */
-.client-js .mw-navigation-hint {
-       overflow: hidden;
-       height: 0;
-       zoom: 1;
-}
-
-.client-nojs #preftoc {
-       display: none;
-}
-
-.client-js #preferences > fieldset {
-       display: none;
-}
-
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
-       display: block;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js
deleted file mode 100644 (file)
index c948ff0..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Tab navigation.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $preferences, tabs, wrapper, previousTab;
-
-               $preferences = $( '#preferences' );
-
-               // Make sure the accessibility tip is selectable so that screen reader users take notice,
-               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
-               // when selected. Similar to jquery.mw-jump
-               $( '<div>' ).addClass( 'mw-navigation-hint' )
-                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
-                       .attr( 'tabIndex', 0 )
-                       .on( 'focus blur', function ( e ) {
-                               if ( e.type === 'blur' || e.type === 'focusout' ) {
-                                       $( this ).css( 'height', '0' );
-                               } else {
-                                       $( this ).css( 'height', 'auto' );
-                               }
-                       } ).prependTo( '#mw-content-text' );
-
-               tabs = new OO.ui.IndexLayout( {
-                       expanded: false,
-                       // Do not remove focus from the tabs menu after choosing a tab
-                       autoFocus: false
-               } );
-
-               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
-                       var panel, $panelContents;
-
-                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
-                               expanded: false,
-                               label: tabConfig.label
-                       } );
-                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
-
-                       // Hide the unnecessary PHP PanelLayouts
-                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
-                       $panelContents.parent().detach();
-
-                       panel.$element.append( $panelContents );
-                       tabs.addTabPanels( [ panel ] );
-
-                       // Remove duplicate labels
-                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
-                       $panelContents.children( 'legend' ).remove();
-                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
-               } );
-
-               wrapper = new OO.ui.PanelLayout( {
-                       expanded: false,
-                       padded: false,
-                       framed: true
-               } );
-               wrapper.$element.append( tabs.$element );
-               $preferences.prepend( wrapper.$element );
-
-               function updateHash( panel ) {
-                       var scrollTop, active;
-                       // Handle hash manually to prevent jumping,
-                       // therefore save and restore scrollTop to prevent jumping.
-                       scrollTop = $( window ).scrollTop();
-                       // Changing the hash apparently causes keyboard focus to be lost?
-                       // Save and restore it. This makes no sense though.
-                       active = document.activeElement;
-                       location.hash = '#mw-prefsection-' + panel.getName();
-                       if ( active ) {
-                               active.focus();
-                       }
-                       $( window ).scrollTop( scrollTop );
-               }
-
-               tabs.on( 'set', updateHash );
-
-               /**
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to supress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       if ( mode === 'noHash' ) {
-                               tabs.off( 'set', updateHash );
-                       }
-                       tabs.setTabPanel( name );
-                       if ( mode === 'noHash' ) {
-                               tabs.on( 'set', updateHash );
-                       }
-               }
-
-               // Jump to correct section as indicated by the hash.
-               // This function is called onload and onhashchange.
-               function detectHash() {
-                       var hash = location.hash,
-                               matchedElement, parentSection;
-                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
-                               mw.storage.session.remove( 'mwpreferences-prevTab' );
-                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
-                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
-                               matchedElement = document.getElementById( hash.slice( 1 ) );
-                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
-                               if ( parentSection.length ) {
-                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-                                       // Switch to proper tab and scroll to selected item.
-                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
-                                       matchedElement.scrollIntoView();
-                               }
-                       }
-               }
-
-               $( window ).on( 'hashchange', function () {
-                       var hash = location.hash;
-                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
-                               detectHash();
-                       } else if ( hash === '' ) {
-                               switchPrefTab( 'personal', 'noHash' );
-                       }
-               } )
-                       // Run the function immediately to select the proper tab on startup.
-                       .trigger( 'hashchange' );
-
-               // Restore the active tab after saving the preferences
-               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
-               if ( previousTab ) {
-                       switchPrefTab( previousTab, 'noHash' );
-                       // Deleting the key, the tab states should be reset until we press Save
-                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-               }
-
-               $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = tabs.getCurrentTabPanelName();
-                       mw.storage.session.set( 'mwpreferences-prevTab', value );
-               } );
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js b/resources/src/mediawiki.special/mediawiki.special.preferences.tabs.legacy.js
deleted file mode 100644 (file)
index 0d97d68..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Tab navigation.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
-
-               labelFunc = function () {
-                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
-               };
-
-               $preftoc = $( '#preftoc' );
-               $preferences = $( '#preferences' );
-
-               $fieldsets = $preferences.children( 'fieldset' )
-                       .attr( {
-                               role: 'tabpanel',
-                               'aria-labelledby': labelFunc
-                       } );
-               $fieldsets.not( '#mw-prefsection-personal' )
-                       .hide()
-                       .attr( 'aria-hidden', 'true' );
-
-               // T115692: The following is kept for backwards compatibility with older skins
-               $preferences.addClass( 'jsprefs' );
-               $fieldsets.addClass( 'prefsection' );
-               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
-               // Make sure the accessibility tip is selectable so that screen reader users take notice,
-               // but hide it per default to reduce interface clutter. Also make sure it becomes visible
-               // when selected. Similar to jquery.mw-jump
-               $( '<div>' ).addClass( 'mw-navigation-hint' )
-                       .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
-                       .attr( 'tabIndex', 0 )
-                       .on( 'focus blur', function ( e ) {
-                               if ( e.type === 'blur' || e.type === 'focusout' ) {
-                                       $( this ).css( 'height', '0' );
-                               } else {
-                                       $( this ).css( 'height', 'auto' );
-                               }
-                       } ).insertBefore( $preftoc );
-
-               /**
-                * It uses document.getElementById for security reasons (HTML injections in $()).
-                *
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to surpress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       var $tab, scrollTop;
-                       // Handle hash manually to prevent jumping,
-                       // therefore save and restore scrollTop to prevent jumping.
-                       scrollTop = $( window ).scrollTop();
-                       if ( mode !== 'noHash' ) {
-                               location.hash = '#mw-prefsection-' + name;
-                       }
-                       $( window ).scrollTop( scrollTop );
-
-                       $preftoc.find( 'li' ).removeClass( 'selected' )
-                               .find( 'a' ).attr( {
-                                       tabIndex: -1,
-                                       'aria-selected': 'false'
-                               } );
-
-                       $tab = $( document.getElementById( 'preftab-' + name ) );
-                       if ( $tab.length ) {
-                               $tab.attr( {
-                                       tabIndex: 0,
-                                       'aria-selected': 'true'
-                               } ).focus()
-                                       .parent().addClass( 'selected' );
-
-                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
-                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
-                       }
-               }
-
-               // Enable keyboard users to use left and right keys to switch tabs
-               $preftoc.on( 'keydown', function ( event ) {
-                       var keyLeft = 37,
-                               keyRight = 39,
-                               $el;
-
-                       if ( event.keyCode === keyLeft ) {
-                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
-                       } else if ( event.keyCode === keyRight ) {
-                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
-                       } else {
-                               return;
-                       }
-                       if ( $el.length > 0 ) {
-                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
-                       }
-               } );
-
-               // Jump to correct section as indicated by the hash.
-               // This function is called onload and onhashchange.
-               function detectHash() {
-                       var hash = location.hash,
-                               matchedElement, parentSection;
-                       if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
-                               mw.storage.session.remove( 'mwpreferences-prevTab' );
-                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
-                       } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
-                               matchedElement = document.getElementById( hash.slice( 1 ) );
-                               parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
-                               if ( parentSection.length ) {
-                                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-                                       // Switch to proper tab and scroll to selected item.
-                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
-                                       matchedElement.scrollIntoView();
-                               }
-                       }
-               }
-
-               $( window ).on( 'hashchange', function () {
-                       var hash = location.hash;
-                       if ( hash.match( /^#mw-[\w-]+/ ) ) {
-                               detectHash();
-                       } else if ( hash === '' ) {
-                               switchPrefTab( 'personal', 'noHash' );
-                       }
-               } )
-                       // Run the function immediately to select the proper tab on startup.
-                       .trigger( 'hashchange' );
-
-               // Restore the active tab after saving the preferences
-               previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
-               if ( previousTab ) {
-                       switchPrefTab( previousTab, 'noHash' );
-                       // Deleting the key, the tab states should be reset until we press Save
-                       mw.storage.session.remove( 'mwpreferences-prevTab' );
-               }
-
-               $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
-                       mw.storage.session.set( 'mwpreferences-prevTab', value );
-               } );
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js b/resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js
deleted file mode 100644 (file)
index a6ffae9..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*!
- * JavaScript for Special:Preferences: Timezone field enhancements.
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $tzSelect, $tzTextbox, timezoneWidget, $localtimeHolder, servertime,
-                       oouiEnabled = $( '#mw-prefs-form' ).hasClass( 'mw-htmlform-ooui' );
-
-               // Timezone functions.
-               // Guesses Timezone from browser and updates fields onchange.
-
-               if ( oouiEnabled ) {
-                       // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
-                       try {
-                               timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
-                       } catch ( err ) {
-                               // This preference could theoretically be disabled ($wgHiddenPrefs)
-                               timezoneWidget = null;
-                       }
-               } else {
-                       $tzSelect = $( '#mw-input-wptimecorrection' );
-                       $tzTextbox = $( '#mw-input-wptimecorrection-other' );
-               }
-
-               $localtimeHolder = $( '#wpLocalTime' );
-               servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
-
-               function minutesToHours( min ) {
-                       var tzHour = Math.floor( Math.abs( min ) / 60 ),
-                               tzMin = Math.abs( min ) % 60,
-                               tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
-                                       ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
-                       return tzString;
-               }
-
-               function hoursToMinutes( hour ) {
-                       var minutes,
-                               arr = hour.split( ':' );
-
-                       arr[ 0 ] = parseInt( arr[ 0 ], 10 );
-
-                       if ( arr.length === 1 ) {
-                               // Specification is of the form [-]XX
-                               minutes = arr[ 0 ] * 60;
-                       } else {
-                               // Specification is of the form [-]XX:XX
-                               minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
-                               if ( arr[ 0 ] < 0 ) {
-                                       minutes *= -1;
-                               }
-                       }
-                       // Gracefully handle non-numbers.
-                       if ( isNaN( minutes ) ) {
-                               return 0;
-                       } else {
-                               return minutes;
-                       }
-               }
-
-               function updateTimezoneSelection() {
-                       var minuteDiff, localTime,
-                               type = oouiEnabled ? timezoneWidget.dropdowninput.getValue() : $tzSelect.val(),
-                               val = oouiEnabled ? timezoneWidget.textinput.getValue() : $tzTextbox.val();
-
-                       if ( type === 'other' ) {
-                               // User specified time zone manually in <input>
-                               // Grab data from the textbox, parse it.
-                               minuteDiff = hoursToMinutes( val );
-                       } else {
-                               // Time zone not manually specified by user
-                               if ( type === 'guess' ) {
-                                       // Get browser timezone & fill it in
-                                       minuteDiff = -( new Date().getTimezoneOffset() );
-                                       if ( oouiEnabled ) {
-                                               timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
-                                               timezoneWidget.dropdowninput.setValue( 'other' );
-                                       } else {
-                                               $tzTextbox.val( minutesToHours( minuteDiff ) );
-                                               $tzSelect.val( 'other' );
-                                       }
-                               } else {
-                                       // Grab data from the dropdown value
-                                       minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
-                               }
-                       }
-
-                       // Determine local time from server time and minutes difference, for display.
-                       localTime = servertime + minuteDiff;
-
-                       // Bring time within the [0,1440) range.
-                       localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
-
-                       $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
-               }
-
-               if ( oouiEnabled ) {
-                       if ( timezoneWidget ) {
-                               timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
-                               timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
-                               updateTimezoneSelection();
-                       }
-               } else {
-                       if ( $tzSelect.length && $tzTextbox.length ) {
-                               $tzSelect.change( updateTimezoneSelection );
-                               $tzTextbox.blur( updateTimezoneSelection );
-                               updateTimezoneSelection();
-                       }
-               }
-
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.recentchanges.js b/resources/src/mediawiki.special/mediawiki.special.recentchanges.js
deleted file mode 100644 (file)
index 29c0fea..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/*!
- * JavaScript for Special:RecentChanges
- */
-( function ( mw, $ ) {
-       var rc, $checkboxes, $select;
-
-       /**
-        * @class mw.special.recentchanges
-        * @singleton
-        */
-       rc = {
-               /**
-                * Handler to disable/enable the namespace selector checkboxes when the
-                * special 'all' namespace is selected/unselected respectively.
-                */
-               updateCheckboxes: function () {
-                       // The option element for the 'all' namespace has an empty value
-                       var isAllNS = $select.val() === '';
-
-                       // Iterates over checkboxes and propagate the selected option
-                       $checkboxes.prop( 'disabled', isAllNS );
-               },
-
-               init: function () {
-                       $select = $( '#namespace' );
-                       $checkboxes = $( '#nsassociated, #nsinvert' );
-
-                       // Bind to change event, and trigger once to set the initial state of the checkboxes.
-                       rc.updateCheckboxes();
-                       $select.change( rc.updateCheckboxes );
-               }
-       };
-
-       $( rc.init );
-
-       module.exports = rc;
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.revisionDelete.js b/resources/src/mediawiki.special/mediawiki.special.revisionDelete.js
deleted file mode 100644 (file)
index cad9db0..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*!
- * JavaScript for Special:RevisionDelete
- */
-( function ( mw, $ ) {
-       var colonSeparator = mw.message( 'colon-separator' ).text(),
-               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-               $wpRevDeleteReasonList = $( '#wpRevDeleteReasonList' ),
-               $wpReason = $( '#wpReason' ),
-               filterFn = function ( input ) {
-                       // Should be built the same as in SpecialRevisionDelete::submit()
-                       var comment = $wpRevDeleteReasonList.val();
-                       if ( comment === 'other' ) {
-                               comment = input;
-                       } else if ( input !== '' ) {
-                               // Entry from drop down menu + additional comment
-                               comment += colonSeparator + input;
-                       }
-                       return comment;
-               };
-
-       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-       if ( summaryCodePointLimit ) {
-               $wpReason.codePointLimit( summaryCodePointLimit, filterFn );
-       } else if ( summaryByteLimit ) {
-               $wpReason.byteLimit( summaryByteLimit, filterFn );
-       }
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js b/resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js
deleted file mode 100644 (file)
index 648bf67..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-( function ( mw, $ ) {
-
-       var api = new mw.Api(),
-               pageUrl = new mw.Uri(),
-               imagesText = new mw.Message( mw.messages, 'searchprofile-images' ),
-               moreResultsText = new mw.Message( mw.messages, 'search-interwiki-more-results' );
-
-       function itemTemplate( results ) {
-
-               var resultOutput = '', i, result, imageCaption, imageThumbnailSrc;
-
-               for ( i = 0; i < results.length; i++ ) {
-                       result = results[ i ];
-                       imageCaption = mw.html.element( 'span', { 'class': 'iw-result__mini-gallery__caption' }, result.title );
-                       imageThumbnailSrc = ( result.thumbnail ) ? result.thumbnail.source : '';
-                       resultOutput += '<div class="iw-result__mini-gallery">' +
-                                               /* escaping response content */
-                                               mw.html.element( 'a', {
-                                                       href: '/wiki/' + result.title,
-                                                       'class': 'iw-result__mini-gallery__image',
-                                                       style: 'background-image: url(' + imageThumbnailSrc + ');'
-                                               }, new mw.html.Raw( imageCaption ) ) +
-                                       '</div>';
-               }
-
-               return resultOutput;
-       }
-
-       function itemWrapperTemplate( pageQuery, itemTemplateOutput ) {
-
-               return '<li class="iw-resultset iw-resultset--image" data-iw-resultset-pos="0">' +
-                               '<div class="iw-result__header">' +
-                                       '<strong>' + imagesText.escaped() + '</strong>' +
-                               '</div>' +
-                               '<div class="iw-result__content">' +
-                               /* template output has been sanitized by mw.html.element */
-                               itemTemplateOutput +
-                               '</div>' +
-                               '<div class="iw-result__footer">' +
-                                       '<a href="/w/index.php?title=Special:Search&search=' + encodeURIComponent( pageQuery ) + '&fulltext=1&profile=images">' +
-                                               moreResultsText.escaped() +
-                                       '</a>' +
-                               '</div>' +
-                       '</li>';
-
-       }
-
-       api.get( {
-               action: 'query',
-               generator: 'search',
-               gsrsearch: pageUrl.query.search,
-               gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file,
-               gsrlimit: 3,
-               prop: 'pageimages',
-               pilimit: 3,
-               piprop: 'thumbnail',
-               pithumbsize: 300,
-               formatversion: 2
-       } ).done( function ( resp ) {
-               var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
-                       multimediaWidgetTemplate;
-
-               if ( !results ) {
-                       return;
-               }
-
-               results.sort( function ( a, b ) {
-                       return a.index - b.index;
-               } );
-
-               multimediaWidgetTemplate = itemWrapperTemplate( pageUrl.query.search, itemTemplate( results ) );
-               /* we really only need to wait for document ready for DOM manipulation */
-               $( function () {
-                       $( '.iw-results' ).append( multimediaWidgetTemplate );
-               } );
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.css b/resources/src/mediawiki.special/mediawiki.special.search.css
deleted file mode 100644 (file)
index aad784e..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-#mw-search-togglebox {
-       float: right;
-}
-#mw-search-togglebox label {
-       margin-right: 0.25em;
-}
-#mw-search-togglebox input {
-       margin-left: 0.25em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.interwikiwidget.styles.less b/resources/src/mediawiki.special/mediawiki.special.search.interwikiwidget.styles.less
deleted file mode 100644 (file)
index 8ec2735..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-/* interwiki search results */
-/*==========================*/
-
-@import 'mediawiki.ui/variables.less';
-@import 'mediawiki.mixins';
-
-.mw-searchresults-has-iw {
-
-       .iw-headline {
-               font-weight: bold;
-       }
-
-       .iw-results {
-               list-style: none;
-               margin: 0;
-       }
-
-       .iw-resultset {
-               .box-sizing(border-box);
-               padding: 0.5em;
-               vertical-align: top;
-               width: 100%;
-               float: left;
-               background-color: @colorGray15;
-               margin-bottom: 1em;
-               word-break: break-word;
-       }
-
-       .iw-result__title {
-               font-size: 108%; /* matching regular search title */
-       }
-
-       .iw-result:after,
-       .iw-result__content:after { /* clearfix */
-               visibility: hidden;
-               display: block;
-               font-size: 0;
-               content: ' ';
-               clear: both;
-               height: 0;
-       }
-
-       .iw-result__footer {
-               float: right;
-               font-size: 97%; /* matching main search result font-size */
-               margin-top: 0.5em;
-       }
-       .iw-result__footer a {
-               vertical-align: middle;
-               font-style: italic;
-       }
-
-       .oo-ui-icon-favicon {
-               padding-right: 1em;
-       }
-
-       /* image search result */
-       .iw-result__mini-gallery {
-               position: relative;
-               float: left;
-               width: 100%;
-               height: 200px;
-               .box-sizing(border-box);
-               padding: 0.25rem;
-       }
-
-       /* second and third images are small */
-       .iw-result__mini-gallery:nth-child( 2 ),
-       .iw-result__mini-gallery:nth-child( 3 ) { /* stylelint-disable-line indentation */
-               width: 50%;
-               height: 100px;
-       }
-
-       .iw-result__mini-gallery__image {
-               display: block;
-               position: relative;
-               width: 100%;
-               height: 100%;
-               background-size: 100% auto;
-               background-size: cover;
-               background-repeat: no-repeat;
-               background-position: center center;
-       }
-
-       /* image gallery text */
-       .iw-result__mini-gallery__image > .iw-result__mini-gallery__caption {
-               visibility: hidden;
-               position: absolute;
-               bottom: 0;
-               left: 0;
-               text-align: center;
-               color: #fff;
-               font-size: 0.8em;
-               padding: 0.5em;
-               background-color: rgba( 0, 0, 0, 0.5 );
-       }
-
-       .iw-result__mini-gallery__image:hover > .iw-result__mini-gallery__caption {
-               visibility: visible;
-       }
-
-       /* tablet and up */
-
-       @media only screen and ( min-width: @deviceWidthTablet ) {
-
-               #mw-interwiki-results {
-                       width: 30%;
-                       display: inline-block; /* used to align interwiki sidebar with the top of the main search results */
-                       margin-left: 8%; /* since inline-block causes whitespace issues, this is 8 instead of 10% */
-               }
-               .mw-search-createlink,
-               .mw-search-nonefound,
-               .mw-search-results,
-               .mw-search-interwiki-header {
-                       float: left;
-                       width: 60%;
-                       clear: left;
-                       max-width: 60%;
-               }
-       }
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.js b/resources/src/mediawiki.special/mediawiki.special.search.js
deleted file mode 100644 (file)
index e809f2e..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*!
- * JavaScript for Special:Search
- */
-( function ( mw, $ ) {
-       $( function () {
-               var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
-
-               // Emulate HTML5 autofocus behavior in non HTML5 compliant browsers
-               if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
-                       $( 'input[autofocus]' ).eq( 0 ).focus();
-               }
-
-               // Create check all/none button
-               $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
-               $( '#mw-search-togglebox' ).append(
-                       $( '<label>' )
-                               .text( mw.msg( 'powersearch-togglelabel' ) )
-               ).append(
-                       $( '<input>' ).attr( 'type', 'button' )
-                               .attr( 'id', 'mw-search-toggleall' )
-                               .prop( 'value', mw.msg( 'powersearch-toggleall' ) )
-                               .click( function () {
-                                       $checkboxes.prop( 'checked', true );
-                               } )
-               ).append(
-                       $( '<input>' ).attr( 'type', 'button' )
-                               .attr( 'id', 'mw-search-togglenone' )
-                               .prop( 'value', mw.msg( 'powersearch-togglenone' ) )
-                               .click( function () {
-                                       $checkboxes.prop( 'checked', false );
-                               } )
-               );
-
-               // Change the header search links to what user entered
-               $headerLinks = $( '.search-types a' );
-               searchWidget = OO.ui.infuse( 'searchText' );
-               updateHeaderLinks = function ( value ) {
-                       $headerLinks.each( function () {
-                               var parts = $( this ).attr( 'href' ).split( 'search=' ),
-                                       lastpart = '',
-                                       prefix = 'search=';
-                               if ( parts.length > 1 && parts[ 1 ].indexOf( '&' ) !== -1 ) {
-                                       lastpart = parts[ 1 ].slice( parts[ 1 ].indexOf( '&' ) );
-                               } else {
-                                       prefix = '&search=';
-                               }
-                               this.href = parts[ 0 ] + prefix + encodeURIComponent( value ) + lastpart;
-                       } );
-               };
-               searchWidget.on( 'change', updateHeaderLinks );
-               updateHeaderLinks( searchWidget.getValue() );
-
-               // When saving settings, use the proper request method (POST instead of GET).
-               $( '#mw-search-powersearch-remember' ).change( function () {
-                       this.form.method = this.checked ? 'post' : 'get';
-               } ).trigger( 'change' );
-
-       } );
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.search.styles.css b/resources/src/mediawiki.special/mediawiki.special.search.styles.css
deleted file mode 100644 (file)
index ea9b987..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-/* Special:Search */
-
-/*
- * Fixes sister projects box moving down the extract
- * of the first result (bug #16886).
- * It only happens when the window is small and
- * This changes slightly the layout for big screens
- * where there was space for the extracts and the
- * sister projects and thus it showed like in any
- * other browser.
- *
- * This will only affect IE 7 and lower
- */
-.searchresult {
-       display: inline !ie;
-}
-.searchresults {
-       margin: 1em 0 1em 0.4em;
-}
-/* needs extra specificity to override `.mw-body p` selector */
-.mw-body .mw-search-nonefound {
-       margin: 0;
-}
-
-.searchdidyoumean em,
-.searchmatch {
-       font-weight: bold;
-}
-
-.mw-search-results {
-       margin: 0;
-       max-width: 38em;
-}
-
-.mw-search-visualclear {
-       clear: both;
-}
-.mw-search-results li {
-       padding-bottom: 1.2em;
-       list-style: none;
-       list-style-image: none;
-}
-.mw-search-results li a {
-       font-size: 108%;
-}
-.mw-search-result-data {
-       color: #008000;
-       font-size: 97%;
-}
-.mw-search-profile-tabs {
-       background-color: #f8f9fa;
-       margin-top: 1em;
-       border: 1px solid #c8ccd1;
-       border-radius: 2px;
-}
-.search-types {
-       float: left;
-       padding-left: 0.25em;
-}
-.search-types ul {
-       margin: 0;
-       padding: 0;
-       list-style: none;
-}
-.search-types li {
-       float: left;
-       margin: 0;
-       padding: 0;
-}
-.search-types a {
-       display: block;
-       padding: 0.5em;
-}
-.search-types .current a {
-       color: #222;
-       cursor: default;
-}
-.search-types .current a:hover {
-       text-decoration: none;
-}
-.results-info {
-       float: right;
-       padding: 0.5em;
-       padding-right: 0.75em;
-       color: #54595d;
-       font-size: 95%;
-}
-#mw-search-top-table div.oo-ui-actionFieldLayout {
-       float: left;
-       width: 100%;
-}
-
-/* Advanced options menu */
-/*==========================*/
-
-#mw-searchoptions {
-       /* Support: Firefox, needs `clear: both` on `fieldset` when zoom level > 100%, see T176499 */
-       clear: both;
-       padding: 0.5em 0.75em 0.75em 0.75em;
-       background-color: #f8f9fa;
-       margin: -1px 0 0;
-       border: 1px solid #c8ccd1;
-       border-radius: 0 0 2px 2px;
-}
-#mw-searchoptions legend {
-       display: none;
-}
-#mw-searchoptions h4 {
-       padding: 0;
-       margin: 0;
-       float: left;
-}
-#mw-searchoptions table {
-       float: left;
-       margin-right: 3em;
-       border-collapse: collapse;
-}
-#mw-searchoptions table td {
-       padding: 0 1em 0 0;
-       white-space: nowrap;
-}
-#mw-searchoptions .divider {
-       clear: both;
-       border-bottom: 1px solid #eaecf0;
-       padding-top: 0.5em;
-       margin-bottom: 0.5em;
-}
-#mw-search-menu {
-       padding-left: 6em;
-       font-size: 85%;
-}
-
-#mw-search-interwiki {
-       float: right;
-       width: 18em;
-       border: 1px solid #a2a9b1;
-       margin-top: 2ex;
-}
-
-.searchalttitle,
-#mw-search-interwiki li {
-       font-size: 95%;
-}
-.mw-search-interwiki-more {
-       float: right;
-       font-size: 90%;
-}
-#mw-search-interwiki-caption {
-       text-align: center;
-       font-weight: bold;
-       font-size: 95%;
-}
-.mw-search-interwiki-project {
-       font-size: 97%;
-       text-align: left;
-       padding: 0.15em 0.15em 0.2em 0.2em;
-       background-color: #eaecf0;
-       border-top: 1px solid #c8ccd1;
-}
-
-.searchdidyoumean {
-       font-size: 127%;
-       margin-top: 0.8em;
-       /* Note that this color won't affect the link, as desired. */
-       color: #d33;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.undelete.js b/resources/src/mediawiki.special/mediawiki.special.undelete.js
deleted file mode 100644 (file)
index e3cf598..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*!
- * JavaScript for Special:Undelete
- */
-( function ( mw, $ ) {
-       $( function () {
-               var summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-                       summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-                       wpComment = OO.ui.infuse( $( '#wpComment' ).closest( '.oo-ui-widget' ) );
-
-               $( '#mw-undelete-invert' ).click( function () {
-                       $( '.mw-undelete-revlist input[type="checkbox"]' ).prop( 'checked', function ( i, val ) {
-                               return !val;
-                       } );
-               } );
-
-               // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-               if ( summaryCodePointLimit ) {
-                       mw.widgets.visibleCodePointLimit( wpComment, summaryCodePointLimit );
-               } else if ( summaryByteLimit ) {
-                       mw.widgets.visibleByteLimit( wpComment, summaryByteLimit );
-               }
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.css
deleted file mode 100644 (file)
index 69fec08..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-.mw-watched-item {
-       text-decoration: line-through;
-}
-
-.mw-watch-link-disabled {
-       pointer-events: none;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js b/resources/src/mediawiki.special/mediawiki.special.unwatchedPages.js
deleted file mode 100644 (file)
index 0886f8c..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*!
- * JavaScript for Special:UnwatchedPages
- */
-( function ( mw, $ ) {
-       $( function () {
-               $( 'a.mw-watch-link' ).click( function ( e ) {
-                       var promise,
-                               api = new mw.Api(),
-                               $link = $( this ),
-                               $subjectLink = $link.closest( 'li' ).children( 'a' ).eq( 0 ),
-                               title = mw.util.getParamValue( 'title', $link.attr( 'href' ) );
-                       // nice format
-                       title = mw.Title.newFromText( title ).toText();
-                       $link.addClass( 'mw-watch-link-disabled' );
-
-                       // Preload the notification module for mw.notify
-                       mw.loader.load( 'mediawiki.notification' );
-
-                       // Use the class to determine whether to watch or unwatch
-                       if ( !$subjectLink.hasClass( 'mw-watched-item' ) ) {
-                               $link.text( mw.msg( 'watching' ) );
-                               promise = api.watch( title ).done( function () {
-                                       $subjectLink.addClass( 'mw-watched-item' );
-                                       $link.text( mw.msg( 'unwatch' ) );
-                                       mw.notify( mw.msg( 'addedwatchtext-short', title ) );
-                               } ).fail( function () {
-                                       $link.text( mw.msg( 'watch' ) );
-                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
-                               } );
-                       } else {
-                               $link.text( mw.msg( 'unwatching' ) );
-                               promise = api.unwatch( title ).done( function () {
-                                       $subjectLink.removeClass( 'mw-watched-item' );
-                                       $link.text( mw.msg( 'watch' ) );
-                                       mw.notify( mw.msg( 'removedwatchtext-short', title ) );
-                               } ).fail( function () {
-                                       $link.text( mw.msg( 'unwatch' ) );
-                                       mw.notify( mw.msg( 'watcherrortext', title ), { type: 'error' } );
-                               } );
-                       }
-
-                       promise.always( function () {
-                               $link.removeClass( 'mw-watch-link-disabled' );
-                       } );
-
-                       e.preventDefault();
-               } );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.js b/resources/src/mediawiki.special/mediawiki.special.upload.js
deleted file mode 100644 (file)
index 144659a..0000000
+++ /dev/null
@@ -1,654 +0,0 @@
-/**
- * JavaScript for Special:Upload
- *
- * @private
- * @class mw.special.upload
- * @singleton
- */
-
-/* global Uint8Array */
-
-( function ( mw, $ ) {
-       var uploadWarning, uploadTemplatePreview,
-               ajaxUploadDestCheck = mw.config.get( 'wgAjaxUploadDestCheck' ),
-               $license = $( '#wpLicense' );
-
-       window.wgUploadWarningObj = uploadWarning = {
-               responseCache: { '': '&nbsp;' },
-               nameToCheck: '',
-               typing: false,
-               delay: 500, // ms
-               timeoutID: false,
-
-               keypress: function () {
-                       if ( !ajaxUploadDestCheck ) {
-                               return;
-                       }
-
-                       // Find file to upload
-                       if ( !$( '#wpDestFile' ).length || !$( '#wpDestFile-warning' ).length ) {
-                               return;
-                       }
-
-                       this.nameToCheck = $( '#wpDestFile' ).val();
-
-                       // Clear timer
-                       if ( this.timeoutID ) {
-                               clearTimeout( this.timeoutID );
-                       }
-                       // Check response cache
-                       if ( this.responseCache.hasOwnProperty( this.nameToCheck ) ) {
-                               this.setWarning( this.responseCache[ this.nameToCheck ] );
-                               return;
-                       }
-
-                       this.timeoutID = setTimeout( function () {
-                               uploadWarning.timeout();
-                       }, this.delay );
-               },
-
-               checkNow: function ( fname ) {
-                       if ( !ajaxUploadDestCheck ) {
-                               return;
-                       }
-                       if ( this.timeoutID ) {
-                               clearTimeout( this.timeoutID );
-                       }
-                       this.nameToCheck = fname;
-                       this.timeout();
-               },
-
-               timeout: function () {
-                       var $spinnerDestCheck, title;
-                       if ( !ajaxUploadDestCheck || this.nameToCheck.trim() === '' ) {
-                               return;
-                       }
-                       $spinnerDestCheck = $.createSpinner().insertAfter( '#wpDestFile' );
-                       title = mw.Title.newFromText( this.nameToCheck, mw.config.get( 'wgNamespaceIds' ).file );
-
-                       ( new mw.Api() ).get( {
-                               formatversion: 2,
-                               action: 'query',
-                               // If title is empty, user input is invalid, the API call will produce details about why
-                               titles: [ title ? title.getPrefixedText() : this.nameToCheck ],
-                               prop: 'imageinfo',
-                               iiprop: 'uploadwarning',
-                               errorformat: 'html',
-                               errorlang: mw.config.get( 'wgUserLanguage' )
-                       } ).done( function ( result ) {
-                               var
-                                       resultOut = '',
-                                       page = result.query.pages[ 0 ];
-                               if ( page.imageinfo ) {
-                                       resultOut = page.imageinfo[ 0 ].html;
-                               } else if ( page.invalidreason ) {
-                                       resultOut = page.invalidreason.html;
-                               }
-                               uploadWarning.processResult( resultOut, uploadWarning.nameToCheck );
-                       } ).always( function () {
-                               $spinnerDestCheck.remove();
-                       } );
-               },
-
-               processResult: function ( result, fileName ) {
-                       this.setWarning( result );
-                       this.responseCache[ fileName ] = result;
-               },
-
-               setWarning: function ( warning ) {
-                       var $warningBox = $( '#wpDestFile-warning' ),
-                               $warning = $( $.parseHTML( warning ) );
-                       mw.hook( 'wikipage.content' ).fire( $warning );
-                       $warningBox.empty().append( $warning );
-
-                       // Set a value in the form indicating that the warning is acknowledged and
-                       // doesn't need to be redisplayed post-upload
-                       if ( !warning ) {
-                               $( '#wpDestFileWarningAck' ).val( '' );
-                               $warningBox.removeAttr( 'class' );
-                       } else {
-                               $( '#wpDestFileWarningAck' ).val( '1' );
-                               $warningBox.attr( 'class', 'mw-destfile-warning' );
-                       }
-
-               }
-       };
-
-       window.wgUploadTemplatePreviewObj = uploadTemplatePreview = {
-
-               responseCache: { '': '' },
-
-               /**
-                * @param {jQuery} $element The element whose .val() will be previewed
-                * @param {jQuery} $previewContainer The container to display the preview in
-                */
-               getPreview: function ( $element, $previewContainer ) {
-                       var template = $element.val(),
-                               $spinner;
-
-                       if ( this.responseCache.hasOwnProperty( template ) ) {
-                               this.showPreview( this.responseCache[ template ], $previewContainer );
-                               return;
-                       }
-
-                       $spinner = $.createSpinner().insertAfter( $element );
-
-                       ( new mw.Api() ).parse( '{{' + template + '}}', {
-                               title: $( '#wpDestFile' ).val() || 'File:Sample.jpg',
-                               prop: 'text',
-                               pst: true,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       } ).done( function ( result ) {
-                               uploadTemplatePreview.processResult( result, template, $previewContainer );
-                       } ).always( function () {
-                               $spinner.remove();
-                       } );
-               },
-
-               processResult: function ( result, template, $previewContainer ) {
-                       this.responseCache[ template ] = result;
-                       this.showPreview( this.responseCache[ template ], $previewContainer );
-               },
-
-               showPreview: function ( preview, $previewContainer ) {
-                       $previewContainer.html( preview );
-               }
-
-       };
-
-       $( function () {
-               // AJAX wpDestFile warnings
-               if ( ajaxUploadDestCheck ) {
-                       // Insert an event handler that fetches upload warnings when wpDestFile
-                       // has been changed
-                       $( '#wpDestFile' ).change( function () {
-                               uploadWarning.checkNow( $( this ).val() );
-                       } );
-                       // Insert a row where the warnings will be displayed just below the
-                       // wpDestFile row
-                       $( '#mw-htmlform-description tbody' ).append(
-                               $( '<tr>' ).append(
-                                       $( '<td>' )
-                                               .attr( 'id', 'wpDestFile-warning' )
-                                               .attr( 'colspan', 2 )
-                               )
-                       );
-               }
-
-               if ( mw.config.get( 'wgAjaxLicensePreview' ) && $license.length ) {
-                       // License selector check
-                       $license.change( function () {
-                               // We might show a preview
-                               uploadTemplatePreview.getPreview( $license, $( '#mw-license-preview' ) );
-                       } );
-
-                       // License selector table row
-                       $license.closest( 'tr' ).after(
-                               $( '<tr>' ).append(
-                                       $( '<td>' ),
-                                       $( '<td>' ).attr( 'id', 'mw-license-preview' )
-                               )
-                       );
-               }
-
-               // fillDestFile setup
-               mw.config.get( 'wgUploadSourceIds' ).forEach( function ( sourceId ) {
-                       $( '#' + sourceId ).change( function () {
-                               var path, slash, backslash, fname;
-                               if ( !mw.config.get( 'wgUploadAutoFill' ) ) {
-                                       return;
-                               }
-                               // Remove any previously flagged errors
-                               $( '#mw-upload-permitted' ).attr( 'class', '' );
-                               $( '#mw-upload-prohibited' ).attr( 'class', '' );
-
-                               path = $( this ).val();
-                               // Find trailing part
-                               slash = path.lastIndexOf( '/' );
-                               backslash = path.lastIndexOf( '\\' );
-                               if ( slash === -1 && backslash === -1 ) {
-                                       fname = path;
-                               } else if ( slash > backslash ) {
-                                       fname = path.slice( slash + 1 );
-                               } else {
-                                       fname = path.slice( backslash + 1 );
-                               }
-
-                               // Clear the filename if it does not have a valid extension.
-                               // URLs are less likely to have a useful extension, so don't include them in the
-                               // extension check.
-                               if (
-                                       mw.config.get( 'wgCheckFileExtensions' ) &&
-                                       mw.config.get( 'wgStrictFileExtensions' ) &&
-                                       Array.isArray( mw.config.get( 'wgFileExtensions' ) ) &&
-                                       $( this ).attr( 'id' ) !== 'wpUploadFileURL'
-                               ) {
-                                       if (
-                                               fname.lastIndexOf( '.' ) === -1 ||
-                                               mw.config.get( 'wgFileExtensions' ).map( function ( element ) {
-                                                       return element.toLowerCase();
-                                               } ).indexOf( fname.slice( fname.lastIndexOf( '.' ) + 1 ).toLowerCase() ) === -1
-                                       ) {
-                                               // Not a valid extension
-                                               // Clear the upload and set mw-upload-permitted to error
-                                               $( this ).val( '' );
-                                               $( '#mw-upload-permitted' ).attr( 'class', 'error' );
-                                               $( '#mw-upload-prohibited' ).attr( 'class', 'error' );
-                                               // Clear wpDestFile as well
-                                               $( '#wpDestFile' ).val( '' );
-
-                                               return false;
-                                       }
-                               }
-
-                               // Replace spaces by underscores
-                               fname = fname.replace( / /g, '_' );
-                               // Capitalise first letter if needed
-                               if ( mw.config.get( 'wgCapitalizeUploads' ) ) {
-                                       fname = fname[ 0 ].toUpperCase() + fname.slice( 1 );
-                               }
-
-                               // Output result
-                               if ( $( '#wpDestFile' ).length ) {
-                                       // Call decodeURIComponent function to remove possible URL-encoded characters
-                                       // from the file name (T32390). Especially likely with upload-form-url.
-                                       // decodeURIComponent can throw an exception if input is invalid utf-8
-                                       try {
-                                               $( '#wpDestFile' ).val( decodeURIComponent( fname ) );
-                                       } catch ( err ) {
-                                               $( '#wpDestFile' ).val( fname );
-                                       }
-                                       uploadWarning.checkNow( fname );
-                               }
-                       } );
-               } );
-       } );
-
-       // Add a preview to the upload form
-       $( function () {
-               /**
-                * Is the FileAPI available with sufficient functionality?
-                *
-                * @return {boolean}
-                */
-               function hasFileAPI() {
-                       return window.FileReader !== undefined;
-               }
-
-               /**
-                * Check if this is a recognizable image type...
-                * Also excludes files over 10M to avoid going insane on memory usage.
-                *
-                * TODO: Is there a way we can ask the browser what's supported in `<img>`s?
-                *
-                * TODO: Put SVG back after working around Firefox 7 bug <https://phabricator.wikimedia.org/T33643>
-                *
-                * @param {File} file
-                * @return {boolean}
-                */
-               function fileIsPreviewable( file ) {
-                       var known = [ 'image/png', 'image/gif', 'image/jpeg', 'image/svg+xml' ],
-                               tooHuge = 10 * 1024 * 1024;
-                       return ( known.indexOf( file.type ) !== -1 ) && file.size > 0 && file.size < tooHuge;
-               }
-
-               /**
-                * Format a file size attractively.
-                *
-                * TODO: Match numeric formatting
-                *
-                * @param {number} s
-                * @return {string}
-                */
-               function prettySize( s ) {
-                       var sizeMsgs = [ 'size-bytes', 'size-kilobytes', 'size-megabytes', 'size-gigabytes' ];
-                       while ( s >= 1024 && sizeMsgs.length > 1 ) {
-                               s /= 1024;
-                               sizeMsgs = sizeMsgs.slice( 1 );
-                       }
-                       return mw.msg( sizeMsgs[ 0 ], Math.round( s ) );
-               }
-
-               /**
-                * Start loading a file into memory; when complete, pass it as a
-                * data URL to the callback function. If the callbackBinary is set it will
-                * first be read as binary and afterwards as data URL. Useful if you want
-                * to do preprocessing on the binary data first.
-                *
-                * @param {File} file
-                * @param {Function} callback
-                * @param {Function} callbackBinary
-                */
-               function fetchPreview( file, callback, callbackBinary ) {
-                       var reader = new FileReader();
-                       if ( callbackBinary && 'readAsBinaryString' in reader ) {
-                               // To fetch JPEG metadata we need a binary string; start there.
-                               // TODO
-                               reader.onload = function () {
-                                       callbackBinary( reader.result );
-
-                                       // Now run back through the regular code path.
-                                       fetchPreview( file, callback );
-                               };
-                               reader.readAsBinaryString( file );
-                       } else if ( callbackBinary && 'readAsArrayBuffer' in reader ) {
-                               // readAsArrayBuffer replaces readAsBinaryString
-                               // However, our JPEG metadata library wants a string.
-                               // So, this is going to be an ugly conversion.
-                               reader.onload = function () {
-                                       var i,
-                                               buffer = new Uint8Array( reader.result ),
-                                               string = '';
-                                       for ( i = 0; i < buffer.byteLength; i++ ) {
-                                               string += String.fromCharCode( buffer[ i ] );
-                                       }
-                                       callbackBinary( string );
-
-                                       // Now run back through the regular code path.
-                                       fetchPreview( file, callback );
-                               };
-                               reader.readAsArrayBuffer( file );
-                       } else if ( 'URL' in window && 'createObjectURL' in window.URL ) {
-                               // Supported in Firefox 4.0 and above <https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL>
-                               // WebKit has it in a namespace for now but that's ok. ;)
-                               //
-                               // Lifetime of this URL is until document close, which is fine
-                               // for Special:Upload -- if this code gets used on longer-running
-                               // pages, add a revokeObjectURL() when it's no longer needed.
-                               //
-                               // Prefer this over readAsDataURL for Firefox 7 due to bug reading
-                               // some SVG files from data URIs <https://bugzilla.mozilla.org/show_bug.cgi?id=694165>
-                               callback( window.URL.createObjectURL( file ) );
-                       } else {
-                               // This ends up decoding the file to base-64 and back again, which
-                               // feels horribly inefficient.
-                               reader.onload = function () {
-                                       callback( reader.result );
-                               };
-                               reader.readAsDataURL( file );
-                       }
-               }
-
-               /**
-                * Clear the file upload preview area.
-                */
-               function clearPreview() {
-                       $( '#mw-upload-thumbnail' ).remove();
-               }
-
-               /**
-                * Show a thumbnail preview of PNG, JPEG, GIF, and SVG files prior to upload
-                * in browsers supporting HTML5 FileAPI.
-                *
-                * As of this writing, known good:
-                *
-                * - Firefox 3.6+
-                * - Chrome 7.something
-                *
-                * TODO: Check file size limits and warn of likely failures
-                *
-                * @param {File} file
-                */
-               function showPreview( file ) {
-                       var $canvas,
-                               ctx,
-                               meta,
-                               previewSize = 180,
-                               $spinner = $.createSpinner( { size: 'small', type: 'block' } )
-                                       .css( { width: previewSize, height: previewSize } ),
-                               thumb = mw.template.get( 'mediawiki.special.upload', 'thumbnail.html' ).render();
-
-                       thumb
-                               .find( '.filename' ).text( file.name ).end()
-                               .find( '.fileinfo' ).text( prettySize( file.size ) ).end()
-                               .find( '.thumbinner' ).prepend( $spinner ).end();
-
-                       $canvas = $( '<canvas>' ).attr( { width: previewSize, height: previewSize } );
-                       ctx = $canvas[ 0 ].getContext( '2d' );
-                       $( '#mw-htmlform-source' ).parent().prepend( thumb );
-
-                       fetchPreview( file, function ( dataURL ) {
-                               var img = new Image(),
-                                       rotation = 0;
-
-                               if ( meta && meta.tiff && meta.tiff.Orientation ) {
-                                       rotation = ( 360 - ( function () {
-                                               // See BitmapHandler class in PHP
-                                               switch ( meta.tiff.Orientation.value ) {
-                                                       case 8:
-                                                               return 90;
-                                                       case 3:
-                                                               return 180;
-                                                       case 6:
-                                                               return 270;
-                                                       default:
-                                                               return 0;
-                                               }
-                                       }() ) ) % 360;
-                               }
-
-                               img.onload = function () {
-                                       var info, width, height, x, y, dx, dy, logicalWidth, logicalHeight;
-
-                                       // Fit the image within the previewSizexpreviewSize box
-                                       if ( img.width > img.height ) {
-                                               width = previewSize;
-                                               height = img.height / img.width * previewSize;
-                                       } else {
-                                               height = previewSize;
-                                               width = img.width / img.height * previewSize;
-                                       }
-                                       // Determine the offset required to center the image
-                                       dx = ( 180 - width ) / 2;
-                                       dy = ( 180 - height ) / 2;
-                                       switch ( rotation ) {
-                                               // If a rotation is applied, the direction of the axis
-                                               // changes as well. You can derive the values below by
-                                               // drawing on paper an axis system, rotate it and see
-                                               // where the positive axis direction is
-                                               case 0:
-                                                       x = dx;
-                                                       y = dy;
-                                                       logicalWidth = img.width;
-                                                       logicalHeight = img.height;
-                                                       break;
-                                               case 90:
-
-                                                       x = dx;
-                                                       y = dy - previewSize;
-                                                       logicalWidth = img.height;
-                                                       logicalHeight = img.width;
-                                                       break;
-                                               case 180:
-                                                       x = dx - previewSize;
-                                                       y = dy - previewSize;
-                                                       logicalWidth = img.width;
-                                                       logicalHeight = img.height;
-                                                       break;
-                                               case 270:
-                                                       x = dx - previewSize;
-                                                       y = dy;
-                                                       logicalWidth = img.height;
-                                                       logicalHeight = img.width;
-                                                       break;
-                                       }
-
-                                       ctx.clearRect( 0, 0, 180, 180 );
-                                       ctx.rotate( rotation / 180 * Math.PI );
-                                       ctx.drawImage( img, x, y, width, height );
-                                       $spinner.replaceWith( $canvas );
-
-                                       // Image size
-                                       info = mw.msg( 'widthheight', logicalWidth, logicalHeight ) +
-                                               ', ' + prettySize( file.size );
-
-                                       $( '#mw-upload-thumbnail .fileinfo' ).text( info );
-                               };
-                               img.onerror = function () {
-                                       // Can happen for example for invalid SVG files
-                                       clearPreview();
-                               };
-                               img.src = dataURL;
-                       }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
-                               var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
-                               try {
-                                       meta = jpegmeta( data, file.fileName );
-                                       // eslint-disable-next-line no-underscore-dangle, camelcase
-                                       meta._binary_data = null;
-                               } catch ( e ) {
-                                       meta = null;
-                               }
-                       } : null );
-               }
-
-               /**
-                * Check if the file does not exceed the maximum size
-                *
-                * @param {File} file
-                * @return {boolean}
-                */
-               function checkMaxUploadSize( file ) {
-                       var maxSize, $error;
-
-                       function getMaxUploadSize( type ) {
-                               var sizes = mw.config.get( 'wgMaxUploadSize' );
-
-                               if ( sizes[ type ] !== undefined ) {
-                                       return sizes[ type ];
-                               }
-                               return sizes[ '*' ];
-                       }
-
-                       $( '.mw-upload-source-error' ).remove();
-
-                       maxSize = getMaxUploadSize( 'file' );
-                       if ( file.size > maxSize ) {
-                               $error = $( '<p class="error mw-upload-source-error" id="wpSourceTypeFile-error">' +
-                                       mw.message( 'largefileserver', file.size, maxSize ).escaped() + '</p>' );
-
-                               $( '#wpUploadFile' ).after( $error );
-
-                               return false;
-                       }
-
-                       return true;
-               }
-
-               /* Initialization */
-               if ( hasFileAPI() ) {
-                       // Update thumbnail when the file selection control is updated.
-                       $( '#wpUploadFile' ).change( function () {
-                               var file;
-                               clearPreview();
-                               if ( this.files && this.files.length ) {
-                                       // Note: would need to be updated to handle multiple files.
-                                       file = this.files[ 0 ];
-
-                                       if ( !checkMaxUploadSize( file ) ) {
-                                               return;
-                                       }
-
-                                       if ( fileIsPreviewable( file ) ) {
-                                               showPreview( file );
-                                       }
-                               }
-                       } );
-               }
-       } );
-
-       // Disable all upload source fields except the selected one
-       $( function () {
-               var $rows = $( '.mw-htmlform-field-UploadSourceField' );
-
-               $rows.on( 'change', 'input[type="radio"]', function ( e ) {
-                       var currentRow = e.delegateTarget;
-
-                       if ( !this.checked ) {
-                               return;
-                       }
-
-                       $( '.mw-upload-source-error' ).remove();
-
-                       // Enable selected upload method
-                       $( currentRow ).find( 'input' ).prop( 'disabled', false );
-
-                       // Disable inputs of other upload methods
-                       // (except for the radio button to re-enable it)
-                       $rows
-                               .not( currentRow )
-                               .find( 'input[type!="radio"]' )
-                               .prop( 'disabled', true );
-               } );
-
-               // Set initial state
-               if ( !$( '#wpSourceTypeurl' ).prop( 'checked' ) ) {
-                       $( '#wpUploadFileURL' ).prop( 'disabled', true );
-               }
-       } );
-
-       $( function () {
-               // Prevent losing work
-               var allowCloseWindow,
-                       $uploadForm = $( '#mw-upload-form' );
-
-               if ( !mw.user.options.get( 'useeditwarning' ) ) {
-                       // If the user doesn't want edit warnings, don't set things up.
-                       return;
-               }
-
-               $uploadForm.data( 'origtext', $uploadForm.serialize() );
-
-               allowCloseWindow = mw.confirmCloseWindow( {
-                       test: function () {
-                               return $( '#wpUploadFile' ).get( 0 ).files.length !== 0 ||
-                                       $uploadForm.data( 'origtext' ) !== $uploadForm.serialize();
-                       },
-
-                       message: mw.msg( 'editwarning-warning' ),
-                       namespace: 'uploadwarning'
-               } );
-
-               $uploadForm.submit( function () {
-                       allowCloseWindow.release();
-               } );
-       } );
-
-       // Add tabindex to mw-editTools
-       $( function () {
-               // Function to change tabindex for all links within mw-editTools
-               function setEditTabindex( $val ) {
-                       $( '.mw-editTools' ).find( 'a' ).each( function () {
-                               $( this ).attr( 'tabindex', $val );
-                       } );
-               }
-
-               // Change tabindex to 0 if user pressed spaced or enter while focused
-               $( '.mw-editTools' ).on( 'keypress', function ( e ) {
-                       // Don't continue if pressed key was not enter or spacebar
-                       if ( e.which !== 13 && e.which !== 32 ) {
-                               return;
-                       }
-
-                       // Change tabindex only when main div has focus
-                       if ( $( this ).is( ':focus' ) ) {
-                               $( this ).find( 'a' ).first().focus();
-                               setEditTabindex( '0' );
-                       }
-               } );
-
-               // Reset tabindex for elements when user focused out mw-editTools
-               $( '.mw-editTools' ).on( 'focusout', function ( e ) {
-                       // Don't continue if relatedTarget is within mw-editTools
-                       if ( e.relatedTarget !== null && $( e.relatedTarget ).closest( '.mw-editTools' ).length > 0 ) {
-                               return;
-                       }
-
-                       // Reset tabindex back to -1
-                       setEditTabindex( '-1' );
-               } );
-
-               // Set initial tabindex for mw-editTools to 0 and to -1 for all links
-               $( '.mw-editTools' ).attr( 'tabindex', '0' );
-               setEditTabindex( '-1' );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.upload.styles.css b/resources/src/mediawiki.special/mediawiki.special.upload.styles.css
deleted file mode 100644 (file)
index 626a7e8..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*!
- * Styling for Special:Upload
- */
-.mw-destfile-warning {
-       border: 1px solid #fde29b;
-       padding: 0.5em 1em;
-       margin-bottom: 1em;
-       color: #705000;
-       background-color: #fdf1d1;
-}
-
-p.mw-upload-editlicenses {
-       font-size: 90%;
-       text-align: right;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.common.css
deleted file mode 100644 (file)
index 2366249..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/* User login and signup forms */
-.mw-ui-vform .mw-form-related-link-container {
-       margin-bottom: 0.5em;
-       text-align: center;
-}
-
-.mw-ui-vform .mw-secure {
-       /* @embed */
-       background: url( images/icon-lock.png ) no-repeat left center;
-       margin: 0 0 0 1px;
-       padding: 0 0 0 11px;
-}
-
-/*
- * When inside the VForm style, disable the border that Vector and other skins
- * put on the div surrounding the login/create account form.
- * Also disable the margin and padding that Vector puts around the form.
- */
-.mw-ui-container #userloginForm,
-.mw-ui-container #userlogin {
-       border: 0;
-       margin: 0;
-       padding: 0;
-}
-
-/* Reposition and resize language links, which appear on a per-wiki basis */
-.mw-ui-container #languagelinks {
-       margin-bottom: 2em;
-       font-size: 0.8em;
-}
-
-/* Put some space under template's header, which may contain CAPTCHA HTML. */
-section.mw-form-header {
-       margin-bottom: 10px;
-}
-
-/* shuffled CAPTCHA */
-#wpCaptchaWord {
-       margin-top: 6px;
-}
-
-.fancycaptcha-captcha-container {
-       background-color: #f8f9fa;
-       margin-bottom: 15px;
-       border: 1px solid #c8ccd1;
-       border-radius: 2px;
-       padding: 8px;
-       text-align: center;
-}
-
-.mw-createacct-captcha-assisted {
-       display: block;
-       margin-top: 0.5em;
-}
-
-/* Put a border around the fancycaptcha-image-container. */
-.fancycaptcha-captcha-and-reload {
-       border: 1px solid #c8ccd1;
-       border-radius: 2px 2px 0 0;
-       /* Other display formats end up too wide */
-       display: table-cell;
-       width: 270px;
-       background-color: #fff;
-}
-
-.fancycaptcha-captcha-container .mw-ui-input {
-       margin-top: -1px;
-       border-color: #c8ccd1;
-       border-radius: 0 0 2px 2px;
-}
-
-/* Make the fancycaptcha-image-container full-width within its parent. */
-.fancycaptcha-image-container {
-       width: 100%;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.login.css
deleted file mode 100644 (file)
index fe013bc..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/* The login form invites users to create an account */
-#mw-createaccount-cta {
-       width: 20em;
-       /* @embed */
-       background: url( images/glyph-people-large.png ) no-repeat 50%;
-       margin: 0 auto;
-       padding-top: 7.8em;
-       font-weight: bold;
-}
-
-/* Login Button, following 'ButtonWidget (progressive)' from OOUI */
-#mw-createaccount-join {
-       background-color: #f8f9fa;
-       color: #36c;
-}
-#mw-createaccount-join:hover {
-       background-color: #fff;
-       border-color: #859ecc;
-       box-shadow: none;
-}
-#mw-createaccount-join:active {
-       background-color: #eff3fa;
-       color: #2a4b8d;
-       border-color: #2a4b8d;
-}
-#mw-createaccount-join:focus {
-       border-color: #36c;
-       box-shadow: inset 0 0 0 1px #36c;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.css
deleted file mode 100644 (file)
index 3cfa5a8..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/* Disable the underline that Vector puts on h2 headings, and bold them. */
-.mw-ui-container h2 {
-       border: 0;
-       font-weight: bold;
-}
-
-/* Benefits column CSS to the right (if it fits) of the form. */
-.mw-ui-container #userloginForm {
-       float: left;
-       /* Override the right margin of the form to give space in case a benefits
-        * column appears to the side. */
-       margin-right: 100px;
-       /* Override `.mw-body-content` to ensure useful, readable paragraphs */
-       line-height: 1.4;
-}
-
-.mw-createacct-benefits-container {
-       /* Keeps this column compact and close to the form, but tends to squish contents. */
-       float: left;
-}
-
-.mw-createacct-benefits-container h2 {
-       margin-bottom: 30px;
-}
-
-.mw-number-text.icon-edits {
-       /* @embed */
-       background: url( images/icon-edits.png ) no-repeat left center;
-}
-
-.mw-number-text.icon-pages {
-       /* @embed */
-       background: url( images/icon-pages.png ) no-repeat left center;
-}
-
-.mw-number-text.icon-contributors {
-       /* @embed */
-       background: url( images/icon-contributors.png ) no-repeat left center;
-}
-
-/*
- * Special font for numbers in benefits, same as Vector's `@content-heading-font-family`.
- * Needs an ID so that it's more specific than Vector's div#content h3.
- */
-#bodyContent .mw-number-text h3 {
-       color: #222;
-       margin: 0;
-       padding: 0;
-       font-family: 'Linux Libertine', 'Georgia', 'Times', serif;
-       font-weight: normal;
-       font-size: 2.2em;
-       line-height: 1.2;
-       text-align: center;
-}
-
-/* Contains a “headlined” number and explanatory text, with space for an icon */
-.mw-number-text {
-       display: block;
-       font-size: 1.2em;
-       color: #444;
-       margin-top: 1em;
-       /* 80px wide icon plus "margin" */
-       padding: 0 0 0 95px;
-       /* Matches max icon height, ensures icon emblem is visible */
-       min-height: 75px;
-       text-align: center;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js b/resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js
deleted file mode 100644 (file)
index 8a61afb..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*!
- * JavaScript for signup form.
- */
-( function ( mw, $ ) {
-       // When sending password by email, hide the password input fields.
-       $( function () {
-               // Always required if checked, otherwise it depends, so we use the original
-               var $emailLabel = $( 'label[for="wpEmail"]' ),
-                       originalText = $emailLabel.text(),
-                       requiredText = mw.message( 'createacct-emailrequired' ).text(),
-                       $createByMailCheckbox = $( '#wpCreateaccountMail' ),
-                       $beforePwds = $( '.mw-row-password:first' ).prev(),
-                       $pwds;
-
-               function updateForCheckbox() {
-                       var checked = $createByMailCheckbox.prop( 'checked' );
-                       if ( checked ) {
-                               $pwds = $( '.mw-row-password' ).detach();
-                               $emailLabel.text( requiredText );
-                       } else {
-                               if ( $pwds ) {
-                                       $beforePwds.after( $pwds );
-                                       $pwds = null;
-                               }
-                               $emailLabel.text( originalText );
-                       }
-               }
-
-               $createByMailCheckbox.on( 'change', updateForCheckbox );
-               updateForCheckbox();
-       } );
-
-       // Check if the username is invalid or already taken
-       mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               var $usernameInput = $root.find( '#wpName2' ),
-                       $passwordInput = $root.find( '#wpPassword2' ),
-                       $emailInput = $root.find( '#wpEmail' ),
-                       $realNameInput = $root.find( '#wpRealName' ),
-                       api = new mw.Api(),
-                       usernameChecker, passwordChecker;
-
-               function checkUsername( username ) {
-                       // We could just use .then() if we didn't have to pass on .abort()…
-                       var d, apiPromise;
-
-                       d = $.Deferred();
-                       apiPromise = api.get( {
-                               action: 'query',
-                               list: 'users',
-                               ususers: username,
-                               usprop: 'cancreate',
-                               formatversion: 2,
-                               errorformat: 'html',
-                               errorsuselocal: true,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       } )
-                               .done( function ( resp ) {
-                                       var userinfo = resp.query.users[ 0 ];
-
-                                       if ( resp.query.users.length !== 1 || userinfo.invalid ) {
-                                               d.resolve( { valid: false, messages: [ mw.message( 'noname' ).parseDom() ] } );
-                                       } else if ( userinfo.userid !== undefined ) {
-                                               d.resolve( { valid: false, messages: [ mw.message( 'userexists' ).parseDom() ] } );
-                                       } else if ( !userinfo.cancreate ) {
-                                               d.resolve( {
-                                                       valid: false,
-                                                       messages: userinfo.cancreateerror ? userinfo.cancreateerror.map( function ( m ) {
-                                                               return m.html;
-                                                       } ) : []
-                                               } );
-                                       } else {
-                                               d.resolve( { valid: true, messages: [] } );
-                                       }
-                               } )
-                               .fail( d.reject );
-
-                       return d.promise( { abort: apiPromise.abort } );
-               }
-
-               function checkPassword() {
-                       // We could just use .then() if we didn't have to pass on .abort()…
-                       var apiPromise,
-                               d = $.Deferred();
-
-                       if ( $usernameInput.val().trim() === '' ) {
-                               d.resolve( { valid: true, messages: [] } );
-                               return d.promise();
-                       }
-
-                       apiPromise = api.post( {
-                               action: 'validatepassword',
-                               user: $usernameInput.val(),
-                               password: $passwordInput.val(),
-                               email: $emailInput.val() || '',
-                               realname: $realNameInput.val() || '',
-                               formatversion: 2,
-                               errorformat: 'html',
-                               errorsuselocal: true,
-                               uselang: mw.config.get( 'wgUserLanguage' )
-                       } )
-                               .done( function ( resp ) {
-                                       var pwinfo = resp.validatepassword || {};
-
-                                       d.resolve( {
-                                               valid: pwinfo.validity === 'Good',
-                                               messages: pwinfo.validitymessages ? pwinfo.validitymessages.map( function ( m ) {
-                                                       return m.html;
-                                               } ) : []
-                                       } );
-                               } )
-                               .fail( d.reject );
-
-                       return d.promise( { abort: apiPromise.abort } );
-               }
-
-               usernameChecker = new mw.htmlform.Checker( $usernameInput, checkUsername );
-               usernameChecker.attach();
-
-               passwordChecker = new mw.htmlform.Checker( $passwordInput, checkPassword );
-               passwordChecker.attach( $usernameInput.add( $emailInput ).add( $realNameInput ) );
-       } );
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.userrights.js b/resources/src/mediawiki.special/mediawiki.special.userrights.js
deleted file mode 100644 (file)
index 487e63a..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-/*!
- * JavaScript for Special:UserRights
- */
-( function ( mw, $ ) {
-       var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' ),
-               summaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' ),
-               summaryByteLimit = mw.config.get( 'wgCommentByteLimit' ),
-               $wpReason = $( '#wpReason' );
-
-       // Replace successbox with notifications
-       convertmessagebox();
-
-       // Dynamically show/hide the "other time" input under each dropdown
-       $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
-               $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
-       } );
-
-       // Limit to bytes or UTF-8 codepoints, depending on MediaWiki's configuration
-       if ( summaryCodePointLimit ) {
-               $wpReason.codePointLimit( summaryCodePointLimit );
-       } else if ( summaryByteLimit ) {
-               $wpReason.byteLimit( summaryByteLimit );
-       }
-
-}( mediaWiki, jQuery ) );
diff --git a/resources/src/mediawiki.special/mediawiki.special.version.css b/resources/src/mediawiki.special/mediawiki.special.version.css
deleted file mode 100644 (file)
index 1b8581a..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*!
- * Styling for Special:Version
- */
-.mw-version-ext-name,
-.mw-version-library-name {
-       font-weight: bold;
-}
-
-.mw-version-ext-license,
-.mw-version-ext-vcs-timestamp {
-       white-space: nowrap;
-}
-
-th.mw-version-ext-col-label {
-       font-size: 0.9em;
-}
-
-.mw-version-ext-vcs-version {
-       unicode-bidi: embed;
-}
-
-.mw-version-credits {
-       column-width: 18em;
-       -moz-column-width: 18em;
-       -webkit-column-width: 18em;
-}
-
-.mw-version-credits ul {
-       margin-top: 0;
-       margin-bottom: 0;
-}
-
-.mw-version-license-info strong {
-       font-weight: normal;
-}
-
-.mw-version-license-info em {
-       font-style: normal;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.css b/resources/src/mediawiki.special/mediawiki.special.watchlist.css
deleted file mode 100644 (file)
index c9861c2..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*!
- * Styling for elements generated by JavaScript on Special:Watchlist
- */
-.mw-changelist-line-inner-unwatched {
-       text-decoration: line-through;
-       opacity: 0.5;
-}
-
-span.mw-changeslist-line-prefix {
-       display: inline-block;
-}
-/* This can be either a span or a table cell */
-.mw-changeslist-line-prefix {
-       width: 1.25em;
-}
diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.js b/resources/src/mediawiki.special/mediawiki.special.watchlist.js
deleted file mode 100644 (file)
index 565ed2c..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-/*!
- * JavaScript for Special:Watchlist
- */
-( function ( mw, $, OO ) {
-       $( function () {
-               var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
-
-               // If the user wants to reset their watchlist, use an API call to do so (no reload required)
-               // Adapted from a user script by User:NQ of English Wikipedia
-               // (User:NQ/WatchlistResetConfirm.js)
-               $resetForm.submit( function ( event ) {
-                       var $button = $resetForm.find( 'input[name=mw-watchlist-reset-submit]' );
-
-                       event.preventDefault();
-
-                       // Disable reset button to prevent multiple concurrent requests
-                       $button.prop( 'disabled', true );
-
-                       if ( !$progressBar ) {
-                               $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
-                               $progressBar.css( {
-                                       position: 'absolute', width: '100%'
-                               } );
-                       }
-                       // Show progress bar
-                       $resetForm.append( $progressBar );
-
-                       // Use action=setnotificationtimestamp to mark all as visited,
-                       // then set all watchlist lines accordingly
-                       api.postWithToken( 'csrf', {
-                               formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
-                       } ).done( function () {
-                               // Enable button again
-                               $button.prop( 'disabled', false );
-                               // Hide the button because further clicks can not generate any visual changes
-                               $button.css( 'visibility', 'hidden' );
-                               $progressBar.detach();
-                               $( '.mw-changeslist-line-watched' )
-                                       .removeClass( 'mw-changeslist-line-watched' )
-                                       .addClass( 'mw-changeslist-line-not-watched' );
-                       } ).fail( function () {
-                               // On error, fall back to server-side reset
-                               // First remove this submit listener and then re-submit the form
-                               $resetForm.off( 'submit' ).submit();
-                       } );
-               } );
-
-               // if the user wishes to reload the watchlist whenever a filter changes
-               if ( mw.user.options.get( 'watchlistreloadautomatically' ) ) {
-                       // add a listener on all form elements in the header form
-                       $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () {
-                               // submit the form when one of the input fields is modified
-                               $( '#mw-watchlist-form' ).submit();
-                       } );
-               }
-
-               if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
-                       // Watch/unwatch toggle link:
-                       // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
-                       // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
-                       // Unwatched page entries are struck through and have lowered opacity.
-                       $( '.mw-changeslist' ).on( 'click', '.mw-unwatch-link, .mw-watch-link', function ( event ) {
-                               var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
-                                       $watchlistLine = $unwatchLink.closest( 'li, table' )
-                                               .find( '[data-target-page]' ),
-                                       pageTitle = $watchlistLine.data( 'targetPage' ),
-                                       isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
-
-                               // Utility function for looping through each watchlist line that matches
-                               // a certain page or its associated page (e.g. Talk)
-                               function forEachMatchingTitle( title, callback ) {
-
-                                       var titleObj = mw.Title.newFromText( title ),
-                                               pageNamespaceId = titleObj.getNamespaceId(),
-                                               isTalk = pageNamespaceId % 2 === 1,
-                                               associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
-                                                       titleObj.getMainText() ).getPrefixedText();
-                                       $( '.mw-changeslist-line' ).each( function () {
-                                               var $this = $( this ), $row, $unwatchLink;
-
-                                               $this.find( '[data-target-page]' ).each( function () {
-                                                       var $this = $( this ), rowTitle = $this.data( 'targetPage' );
-                                                       if ( rowTitle === title || rowTitle === associatedTitle ) {
-
-                                                               // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
-                                                               // * If using OldChangesList, use the <li>
-                                                               // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
-                                                               // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
-                                                               $row =
-                                                                       $this.closest(
-                                                                               'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
-                                                               $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
-
-                                                               callback( rowTitle, $row, $unwatchLink );
-                                                       }
-                                               } );
-                                       } );
-                               }
-
-                               // Preload the notification module for mw.notify
-                               mw.loader.load( 'mediawiki.notification' );
-
-                               // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
-                               // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
-                               if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
-                                       api.unwatch( pageTitle )
-                                               .done( function () {
-                                                       forEachMatchingTitle( pageTitle,
-                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
-                                                                       $rowUnwatchLink
-                                                                               .text( mw.msg( 'watchlist-unwatch-undo' ) )
-                                                                               .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
-                                                                               .attr( 'href',
-                                                                                       mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
-                                                                               .removeClass( 'mw-unwatch-link loading' )
-                                                                               .addClass( 'mw-watch-link' );
-                                                                       $row.find(
-                                                                               '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
-                                                                               .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
-                                                                               .addClass( 'mw-changelist-line-inner-unwatched' );
-                                                               } );
-
-                                                       mw.notify(
-                                                               mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
-                                                                       pageTitle ), { tag: 'watch-self' } );
-                                               } );
-                               } else {
-                                       api.watch( pageTitle )
-                                               .then( function () {
-                                                       forEachMatchingTitle( pageTitle,
-                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
-                                                                       $rowUnwatchLink
-                                                                               .text( mw.msg( 'watchlist-unwatch' ) )
-                                                                               .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
-                                                                               .attr( 'href',
-                                                                                       mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
-                                                                               .removeClass( 'mw-watch-link loading' )
-                                                                               .addClass( 'mw-unwatch-link' );
-                                                                       $row.find( '.mw-changelist-line-inner-unwatched' )
-                                                                               .addBack( '.mw-enhanced-rc-nested' )
-                                                                               .removeClass( 'mw-changelist-line-inner-unwatched' );
-                                                               } );
-
-                                                       mw.notify(
-                                                               mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
-                                                                       pageTitle ), { tag: 'watch-self' } );
-                                               } );
-                               }
-
-                               event.preventDefault();
-                               event.stopPropagation();
-                               $unwatchLink.blur();
-                       } );
-               }
-       } );
-
-}( mediaWiki, jQuery, OO )
-);
diff --git a/resources/src/mediawiki.special/templates/thumbnail.html b/resources/src/mediawiki.special/templates/thumbnail.html
deleted file mode 100644 (file)
index bf0e701..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<div id="mw-upload-thumbnail" class="thumb tright">
-       <div class="thumbinner">
-               <div class="thumbcaption">
-                       <div class="filename"></div>
-                       <div class="fileinfo"></div>
-               </div>
-       </div>
-</div>
index b1e1da3..fbe8af2 100644 (file)
                        // Override #set to also set the global variable
                        this.set = function ( selection, value ) {
                                var s;
-
-                               if ( $.isPlainObject( selection ) ) {
-                                       for ( s in selection ) {
-                                               setGlobalMapValue( this, s, selection[ s ] );
+                               if ( arguments.length > 1 ) {
+                                       if ( typeof selection !== 'string' ) {
+                                               return false;
                                        }
+                                       setGlobalMapValue( this, selection, value );
                                        return true;
                                }
-                               if ( typeof selection === 'string' && arguments.length ) {
-                                       setGlobalMapValue( this, selection, value );
+                               if ( typeof selection === 'object' ) {
+                                       for ( s in selection ) {
+                                               setGlobalMapValue( this, s, selection[ s ] );
+                                       }
                                        return true;
                                }
                                return false;
                 */
                set: function ( selection, value ) {
                        var s;
-
-                       if ( $.isPlainObject( selection ) ) {
-                               for ( s in selection ) {
-                                       this.values[ s ] = selection[ s ];
+                       // Use `arguments.length` because `undefined` is also a valid value.
+                       if ( arguments.length > 1 ) {
+                               if ( typeof selection !== 'string' ) {
+                                       return false;
                                }
+                               this.values[ selection ] = value;
                                return true;
                        }
-                       if ( typeof selection === 'string' && arguments.length > 1 ) {
-                               this.values[ selection ] = value;
+                       if ( typeof selection === 'object' ) {
+                               for ( s in selection ) {
+                                       this.values[ s ] = selection[ s ];
+                               }
                                return true;
                        }
                        return false;
diff --git a/tests/phpunit/data/categoriesrdf/change.sparql b/tests/phpunit/data/categoriesrdf/change.sparql
new file mode 100644 (file)
index 0000000..d7ec83a
--- /dev/null
@@ -0,0 +1,16 @@
+# Changes
+DELETE {
+?category ?x ?y
+} INSERT {
+
+<http://acme.test/wiki/Category:Changed_category> a mediawiki:Category ;
+       rdfs:label "Changed category" ;
+       mediawiki:pages "7"^^xsd:integer ;
+       mediawiki:subcategories "2"^^xsd:integer ;
+       mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_30> .
+
+} WHERE {
+   VALUES ?category {
+     <http://acme.test/wiki/Category:Changed_category>
+   }
+};
diff --git a/tests/phpunit/data/categoriesrdf/delete.sparql b/tests/phpunit/data/categoriesrdf/delete.sparql
new file mode 100644 (file)
index 0000000..7fb642d
--- /dev/null
@@ -0,0 +1,10 @@
+# Deletes
+DELETE {
+?category ?x ?y
+} INSERT {
+
+} WHERE {
+   VALUES ?category {
+     <http://acme.test/wiki/Category:Test> <http://acme.test/wiki/Category:Test_2>
+   }
+};
diff --git a/tests/phpunit/data/categoriesrdf/move.sparql b/tests/phpunit/data/categoriesrdf/move.sparql
new file mode 100644 (file)
index 0000000..c9f284e
--- /dev/null
@@ -0,0 +1,24 @@
+# Moves
+DELETE {
+?category ?x ?y
+} INSERT {
+
+<http://acme.test/wiki/Category:MovedTo> a mediawiki:Category ;
+       rdfs:label "MovedTo" ;
+       mediawiki:pages "7"^^xsd:integer ;
+       mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:AlsoMoved> a mediawiki:Category ;
+       rdfs:label "AlsoMoved" ;
+       mediawiki:pages "7"^^xsd:integer ;
+       mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:MovedTo> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_4> .
+
+<http://acme.test/wiki/Category:AlsoMoved> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_5> .
+
+} WHERE {
+   VALUES ?category {
+     <http://acme.test/wiki/Category:Test> <http://acme.test/wiki/Category:MovedTo> <http://acme.test/wiki/Category:Test_2> <http://acme.test/wiki/Category:Test_3> <http://acme.test/wiki/Category:Test_4>
+   }
+};
diff --git a/tests/phpunit/data/categoriesrdf/new.sparql b/tests/phpunit/data/categoriesrdf/new.sparql
new file mode 100644 (file)
index 0000000..f9a742d
--- /dev/null
@@ -0,0 +1,19 @@
+# Additions
+INSERT DATA {
+
+<http://acme.test/wiki/Category:New_category> a mediawiki:Category ;
+       rdfs:label "New category" ;
+       mediawiki:pages "7"^^xsd:integer ;
+       mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:%D0%9D%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F_%F0%9F%98%83> a mediawiki:Category,
+               mediawiki:HiddenCategory ;
+       rdfs:label "Новая категория 😃" ;
+       mediawiki:pages "7"^^xsd:integer ;
+       mediawiki:subcategories "2"^^xsd:integer .
+
+<http://acme.test/wiki/Category:New_category> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_20> .
+
+<http://acme.test/wiki/Category:%D0%9D%D0%BE%D0%B2%D0%B0%D1%8F_%D0%BA%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%8F_%F0%9F%98%83> mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_21> .
+
+};
diff --git a/tests/phpunit/data/categoriesrdf/restore.sparql b/tests/phpunit/data/categoriesrdf/restore.sparql
new file mode 100644 (file)
index 0000000..16c0561
--- /dev/null
@@ -0,0 +1,10 @@
+# Restores
+INSERT DATA {
+
+<http://acme.test/wiki/Category:Restored_cat> a mediawiki:Category ;
+       rdfs:label "Restored cat" ;
+       mediawiki:pages "7"^^xsd:integer ;
+       mediawiki:subcategories "2"^^xsd:integer ;
+       mediawiki:isInCategory <http://acme.test/wiki/Category:Parent_of_10> .
+
+};
diff --git a/tests/phpunit/data/categoriesrdf/updatets.txt b/tests/phpunit/data/categoriesrdf/updatets.txt
new file mode 100644 (file)
index 0000000..426bb92
--- /dev/null
@@ -0,0 +1,9 @@
+DELETE {
+  <http://acme.test/wiki/Special:CategoryDump> schema:dateModified ?o .
+}
+WHERE {
+  <http://acme.test/wiki/Special:CategoryDump> schema:dateModified ?o .
+};
+INSERT DATA {
+  <http://acme.test/wiki/Special:CategoryDump> schema:dateModified "2017-08-25T00:29:09Z"^^xsd:dateTime .
+}
index a84cc04..6e23e53 100644 (file)
@@ -153,64 +153,75 @@ class LBFactoryTest extends MediaWikiTestCase {
                $lb->closeAll();
        }
 
-       public function testLBFactoryMulti() {
-               global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
+       public function testLBFactoryMultiConns() {
+               $factory = $this->newLBFactoryMultiLBs();
 
-               $factory = new LBFactoryMulti( [
-                       'sectionsByDB' => [
-                               's1wiki' => 's1',
-                       ],
-                       'sectionLoads' => [
-                               's1' => [
-                                       'test-db3' => 0,
-                                       'test-db4' => 100,
-                               ],
-                               'DEFAULT' => [
-                                       'test-db1' => 0,
-                                       'test-db2' => 100,
-                               ]
-                       ],
-                       'serverTemplate' => [
-                               'dbname'      => $wgDBname,
-                               'user'        => $wgDBuser,
-                               'password'    => $wgDBpassword,
-                               'type'        => $wgDBtype,
-                               'dbDirectory' => $wgSQLiteDataDir,
-                               'flags'       => DBO_DEFAULT
-                       ],
-                       'hostsByName' => [
-                               'test-db1'  => $wgDBserver,
-                               'test-db2'  => $wgDBserver,
-                               'test-db3'  => $wgDBserver,
-                               'test-db4'  => $wgDBserver
-                       ],
-                       'loadMonitorClass' => LoadMonitorNull::class
-               ] );
-               $lb = $factory->getMainLB();
-
-               $dbw = $lb->getConnection( DB_MASTER );
+               $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
                $this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
 
-               $dbr = $lb->getConnection( DB_REPLICA );
+               $dbr = $factory->getMainLB()->getConnection( DB_REPLICA );
                $this->assertTrue( $dbr->getLBInfo( 'replica' ), 'slave shows as slave' );
 
-               // Test that LoadBalancer instances made during commitMasterChanges() do not throw
-               // DBTransactionError due to transaction ROUND_* stages being mismatched.
+               // Destructor should trigger without round stage errors
+               unset( $factory );
+       }
+
+       public function testLBFactoryMultiRoundCallbacks() {
+               $called = 0;
+               $countLBsFunc = function ( LBFactoryMulti $factory ) {
+                       $count = 0;
+                       $factory->forEachLB( function () use ( &$count ) {
+                               ++$count;
+                       } );
+
+                       return $count;
+               };
+
+               $factory = $this->newLBFactoryMultiLBs();
+               $this->assertEquals( 0, $countLBsFunc( $factory ) );
+               $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
+               $this->assertEquals( 1, $countLBsFunc( $factory ) );
+               // Test that LoadBalancer instances made during pre-commit callbacks in do not
+               // throw DBTransactionError due to transaction ROUND_* stages being mismatched.
                $factory->beginMasterChanges( __METHOD__ );
-               $dbw->onTransactionPreCommitOrIdle( function () use ( $factory ) {
+               $dbw->onTransactionPreCommitOrIdle( function () use ( $factory, &$called ) {
+                       ++$called;
                        // Trigger s1 LoadBalancer instantiation during "finalize" stage.
                        // There is no s1wiki DB to select so it is not in getConnection(),
                        // but this fools getMainLB() at least.
                        $factory->getMainLB( 's1wiki' )->getConnection( DB_MASTER );
                } );
                $factory->commitMasterChanges( __METHOD__ );
+               $this->assertEquals( 1, $called );
+               $this->assertEquals( 2, $countLBsFunc( $factory ) );
+               $factory->shutdown();
+               $factory->closeAll();
 
-               $count = 0;
-               $factory->forEachLB( function () use ( &$count ) {
-                       ++$count;
+               $called = 0;
+               $factory = $this->newLBFactoryMultiLBs();
+               $this->assertEquals( 0, $countLBsFunc( $factory ) );
+               $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
+               $this->assertEquals( 1, $countLBsFunc( $factory ) );
+               // Test that LoadBalancer instances made during pre-commit callbacks in do not
+               // throw DBTransactionError due to transaction ROUND_* stages being mismatched.hrow
+               // DBTransactionError due to transaction ROUND_* stages being mismatched.
+               $factory->beginMasterChanges( __METHOD__ );
+               $dbw->query( "SELECT 1 as t", __METHOD__ );
+               $dbw->onTransactionResolution( function () use ( $factory, &$called ) {
+                       ++$called;
+                       // Trigger s1 LoadBalancer instantiation during "finalize" stage.
+                       // There is no s1wiki DB to select so it is not in getConnection(),
+                       // but this fools getMainLB() at least.
+                       $factory->getMainLB( 's1wiki' )->getConnection( DB_MASTER );
                } );
-               $this->assertEquals( 2, $count );
+               $factory->commitMasterChanges( __METHOD__ );
+               $this->assertEquals( 1, $called );
+               $this->assertEquals( 2, $countLBsFunc( $factory ) );
+               $factory->shutdown();
+               $factory->closeAll();
 
+               $factory = $this->newLBFactoryMultiLBs();
+               $dbw = $factory->getMainLB()->getConnection( DB_MASTER );
                // DBTransactionError should not be thrown
                $ran = 0;
                $dbw->onTransactionPreCommitOrIdle( function () use ( &$ran ) {
@@ -223,6 +234,41 @@ class LBFactoryTest extends MediaWikiTestCase {
                $factory->closeAll();
        }
 
+       private function newLBFactoryMultiLBs() {
+               global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
+
+               return new LBFactoryMulti( [
+                       'sectionsByDB' => [
+                               's1wiki' => 's1',
+                       ],
+                       'sectionLoads' => [
+                               's1' => [
+                                       'test-db3' => 0,
+                                       'test-db4' => 100,
+                               ],
+                               'DEFAULT' => [
+                                       'test-db1' => 0,
+                                       'test-db2' => 100,
+                               ]
+                       ],
+                       'serverTemplate' => [
+                               'dbname' => $wgDBname,
+                               'user' => $wgDBuser,
+                               'password' => $wgDBpassword,
+                               'type' => $wgDBtype,
+                               'dbDirectory' => $wgSQLiteDataDir,
+                               'flags' => DBO_DEFAULT
+                       ],
+                       'hostsByName' => [
+                               'test-db1' => $wgDBserver,
+                               'test-db2' => $wgDBserver,
+                               'test-db3' => $wgDBserver,
+                               'test-db4' => $wgDBserver
+                       ],
+                       'loadMonitorClass' => LoadMonitorNull::class
+               ] );
+       }
+
        /**
         * @covers \Wikimedia\Rdbms\ChronologyProtector
         */
diff --git a/tests/phpunit/maintenance/categoryChangesRdfTest.php b/tests/phpunit/maintenance/categoryChangesRdfTest.php
new file mode 100644 (file)
index 0000000..30a56f4
--- /dev/null
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * Tests for CategoryChangesAsRdf recent changes exporter.
+ *  @covers CategoryChangesAsRdf
+ */
+class CategoryChangesRdfTest extends MediaWikiLangTestCase {
+
+       public function setUp() {
+               parent::setUp();
+               $this->setMwGlobals( [
+                       'wgServer' => 'http://acme.test',
+                       'wgCanonicalServer' => 'http://acme.test',
+                       'wgArticlePath' => '/wiki/$1',
+               ] );
+       }
+
+       public function provideCategoryData() {
+               return [
+                       'delete category' => [
+                               __DIR__ . "/../data/categoriesrdf/delete.sparql",
+                               'getDeletedCatsIterator',
+                               'handleDeletes',
+                               [
+                                       (object)[ 'rc_title' => 'Test', 'rc_cur_id' => 1, '_processed' => 1 ],
+                                       (object)[ 'rc_title' => 'Test 2', 'rc_cur_id' => 2, '_processed' => 2 ],
+                               ],
+                       ],
+                       'move category' => [
+                               __DIR__ . "/../data/categoriesrdf/move.sparql",
+                               'getMovedCatsIterator',
+                               'handleMoves',
+                               [
+                                       (object)[
+                                               'rc_title' => 'Test',
+                                               'rc_cur_id' => 4,
+                                               'page_title' => 'MovedTo',
+                                               'page_namespace' => NS_CATEGORY,
+                                               '_processed' => 4,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'MovedTo',
+                                               'rc_cur_id' => 4,
+                                               'page_title' => 'MovedAgain',
+                                               'page_namespace' => NS_CATEGORY,
+                                               'pp_propname' => 'hiddencat',
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Test 2',
+                                               'rc_cur_id' => 5,
+                                               'page_title' => 'AlsoMoved',
+                                               'page_namespace' => NS_CATEGORY,
+                                               '_processed' => 5,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Test 3',
+                                               'rc_cur_id' => 6,
+                                               'page_title' => 'MovedOut',
+                                               'page_namespace' => NS_MAIN,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Test 4',
+                                               'rc_cur_id' => 7,
+                                               'page_title' => 'Already Done',
+                                               'page_namespace' => NS_CATEGORY,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                               ],
+                               [ 7 => true ],
+                       ],
+                       'restore deleted category' => [
+                               __DIR__ . "/../data/categoriesrdf/restore.sparql",
+                               'getRestoredCatsIterator',
+                               'handleRestores',
+                               [
+                                       (object)[
+                                               'rc_title' => 'Restored cat',
+                                               'rc_cur_id' => 10,
+                                               '_processed' => 10,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Restored again',
+                                               'rc_cur_id' => 10,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Already seen',
+                                               'rc_cur_id' => 11,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                               ],
+                               [ 11 => true ],
+                       ],
+                       'new page' => [
+                               __DIR__ . "/../data/categoriesrdf/new.sparql",
+                               'getNewCatsIterator',
+                               'handleAdds',
+                               [
+                                       (object)[
+                                               'rc_title' => 'New category',
+                                               'rc_cur_id' => 20,
+                                               '_processed' => 20,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Новая категория 😃',
+                                               'rc_cur_id' => 21,
+                                               '_processed' => 21,
+                                               'pp_propname' => 'hiddencat',
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Processed already',
+                                               'rc_cur_id' => 22,
+                                       ],
+                               ],
+                               [ 22 => true ],
+                       ],
+                       'change in categories' => [
+                               __DIR__ . "/../data/categoriesrdf/change.sparql",
+                               'getChangedCatsIterator',
+                               'handleChanges',
+                               [
+                                       (object)[
+                                               'rc_title' => 'Changed category',
+                                               'rc_cur_id' => 30,
+                                               '_processed' => 30,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Changed again',
+                                               'rc_cur_id' => 30,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                                       (object)[
+                                               'rc_title' => 'Processed already',
+                                               'rc_cur_id' => 31,
+                                               'pp_propname' => null,
+                                               'cat_pages' => 10,
+                                               'cat_subcats' => 2,
+                                               'cat_files' => 1,
+                                       ],
+                               ],
+                               [ 31 => true ],
+                       ],
+
+               ];
+       }
+
+       /**
+        * Mock category links iterator.
+        * @param $dbr
+        * @param array $ids
+        * @return array
+        */
+       public function getCategoryLinksIterator( $dbr, array $ids ) {
+               $res = [];
+               foreach ( $ids as $pageid ) {
+                       $res[] = (object)[ 'cl_from' => $pageid, 'cl_to' => "Parent of $pageid" ];
+               }
+               return $res;
+       }
+
+       /**
+        * @dataProvider provideCategoryData
+        * @param string $testFileName Name of the test, defines filename with expected results.
+        * @param string $iterator Iterator method name to mock
+        * @param string $handler Handler method to call
+        * @param array $result Result to be returned from mock iterator
+        * @param array $preProcessed List of pre-processed items
+        */
+       public function testSparqlUpdate( $testFileName, $iterator, $handler, $result,
+                       array $preProcessed = [] ) {
+               $dumpScript =
+                       $this->getMockBuilder( CategoryChangesAsRdf::class )
+                               ->setMethods( [ $iterator, 'getCategoryLinksIterator' ] )
+                               ->getMock();
+
+               $dumpScript->expects( $this->any() )
+                       ->method( 'getCategoryLinksIterator' )
+                       ->willReturnCallback( [ $this, 'getCategoryLinksIterator' ] );
+
+               $dumpScript->expects( $this->once() )
+                       ->method( $iterator )
+                       ->willReturn( [ $result ] );
+
+               $ref = new ReflectionObject( $dumpScript );
+               $processedProperty = $ref->getProperty( 'processed' );
+               $processedProperty->setAccessible( true );
+               $processedProperty->setValue( $dumpScript, $preProcessed );
+
+               $output = fopen( "php://memory", "w+b" );
+               $dbr = wfGetDB( DB_REPLICA );
+               /** @var CategoryChangesAsRdf $dumpScript */
+               $dumpScript->initialize();
+               $dumpScript->getRdf();
+               $dumpScript->$handler( $dbr, $output );
+
+               rewind( $output );
+               $sparql = stream_get_contents( $output );
+               $this->assertFileContains( $testFileName, $sparql );
+
+               $processed = $processedProperty->getValue( $dumpScript );
+               $expectedProcessed = $preProcessed;
+               foreach ( $result as $row ) {
+                       if ( isset( $row->_processed ) ) {
+                               $this->assertArrayHasKey( $row->_processed, $processed,
+                                       "ID {$row->_processed} was not processed!" );
+                               $expectedProcessed[] = $row->_processed;
+                       }
+               }
+               $this->assertArrayEquals( $expectedProcessed, array_keys( $processed ),
+                       'Processed array has wrong items' );
+       }
+
+       public function testUpdateTs() {
+               $dumpScript = new CategoryChangesAsRdf();
+               $dumpScript->initialize();
+               $update = $dumpScript->updateTS( 1503620949 );
+               $outFile = __DIR__ . '/../data/categoriesrdf/updatets.txt';
+               $this->assertFileContains( $outFile, $update );
+       }
+
+}
index 119222a..75dc665 100644 (file)
                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)' );
                assert.strictEqual( conf.set( nummy, 'Nummy' ), false, 'Map.set returns boolean false if key was invalid (Number)' );
+               assert.strictEqual( conf.set( null, 'Null' ), false, 'Map.set returns false if key is invalid (null)' );
+               assert.strictEqual( conf.set( {}, 'Object' ), false, 'Map.set returns false if key is invalid (plain object)' );
 
                conf.set( String( nummy ), 'I used to be a number' );