Merge "Hard deprecate Language::setCode"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sun, 10 Feb 2019 06:03:08 +0000 (06:03 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sun, 10 Feb 2019 06:03:08 +0000 (06:03 +0000)
540 files changed:
.phpcs.xml
Gruntfile.js
RELEASE-NOTES-1.33
autoload.php
composer.json
docs/extension.schema.v1.json
docs/extension.schema.v2.json
docs/hooks.txt
includes/AutoLoader.php
includes/Block.php
includes/CommentStore.php
includes/DefaultSettings.php
includes/DummyLinker.php
includes/EditPage.php
includes/EventRelayerGroup.php
includes/FileDeleteForm.php
includes/GitInfo.php
includes/GlobalFunctions.php
includes/Linker.php
includes/MediaWiki.php
includes/MovePage.php
includes/ProtectionForm.php
includes/Revision.php
includes/Revision/RevisionRecord.php
includes/ServiceWiring.php
includes/Setup.php
includes/Title.php
includes/WikiMap.php
includes/XmlJsCode.php
includes/actions/McrUndoAction.php
includes/api/ApiBase.php
includes/api/ApiQueryInfo.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiRevisionDelete.php
includes/api/i18n/ar.json
includes/api/i18n/ba.json
includes/api/i18n/cs.json
includes/api/i18n/de.json
includes/api/i18n/en-gb.json
includes/api/i18n/en.json
includes/api/i18n/es.json
includes/api/i18n/eu.json
includes/api/i18n/fa.json
includes/api/i18n/fr.json
includes/api/i18n/gl.json
includes/api/i18n/he.json
includes/api/i18n/hu.json
includes/api/i18n/it.json
includes/api/i18n/ja.json
includes/api/i18n/ko.json
includes/api/i18n/ksh.json
includes/api/i18n/lb.json
includes/api/i18n/lt.json
includes/api/i18n/mk.json
includes/api/i18n/nb.json
includes/api/i18n/nl.json
includes/api/i18n/pl.json
includes/api/i18n/pt-br.json
includes/api/i18n/pt.json
includes/api/i18n/qqq.json
includes/api/i18n/ru.json
includes/api/i18n/sv.json
includes/api/i18n/uk.json
includes/api/i18n/zh-hans.json
includes/api/i18n/zh-hant.json
includes/cache/CacheHelper.php
includes/cache/ICacheHelper.php [new file with mode: 0644]
includes/cache/LinkCache.php
includes/cache/localisation/LocalisationCache.php
includes/changes/ChangesListBooleanFilter.php
includes/changes/ChangesListFilterGroup.php
includes/changetags/ChangeTags.php
includes/changetags/ChangeTagsLogList.php
includes/changetags/ChangeTagsRevisionList.php
includes/content/JsonContent.php
includes/context/RequestContext.php
includes/debug/logger/LegacyLogger.php
includes/debug/logger/monolog/WikiProcessor.php
includes/deferred/CdnCacheUpdate.php
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/diff/DifferenceEngine.php
includes/exception/ILocalizedException.php [new file with mode: 0644]
includes/exception/LocalizedException.php
includes/exception/MWExceptionHandler.php
includes/export/BaseDump.php
includes/export/XmlDumpWriter.php
includes/filebackend/lockmanager/LockManagerGroup.php
includes/filerepo/file/LocalFile.php
includes/gallery/ImageGalleryBase.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/http/GuzzleHttpRequest.php
includes/http/Http.php
includes/http/HttpRequestFactory.php
includes/http/MWHttpRequest.php
includes/import/WikiImporter.php
includes/installer/DatabaseUpdater.php
includes/installer/MssqlUpdater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/installer/i18n/be-tarask.json
includes/installer/i18n/bn.json
includes/installer/i18n/cs.json
includes/installer/i18n/en.json
includes/installer/i18n/es.json
includes/installer/i18n/fi.json
includes/installer/i18n/ja.json
includes/installer/i18n/nl.json
includes/installer/i18n/ru.json
includes/installer/i18n/sr-el.json
includes/installer/i18n/tcy.json
includes/interwiki/ClassicInterwikiLookup.php
includes/jobqueue/IJobSpecification.php [new file with mode: 0644]
includes/jobqueue/JobQueue.php
includes/jobqueue/JobQueueFederated.php
includes/jobqueue/JobQueueGroup.php
includes/jobqueue/JobQueueRedis.php
includes/jobqueue/JobSpecification.php
includes/jobqueue/exception/JobQueueConnectionError.php [new file with mode: 0644]
includes/jobqueue/exception/JobQueueError.php [new file with mode: 0644]
includes/jobqueue/exception/JobQueueReadOnlyError.php [new file with mode: 0644]
includes/libs/filebackend/FSFileBackend.php
includes/libs/mime/MSCompoundFileReader.php
includes/libs/mime/MimeAnalyzer.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/objectcache/WinCacheBagOStuff.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/lbfactory/ILBFactory.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/rdbms/lbfactory/LBFactoryMulti.php
includes/libs/rdbms/lbfactory/LBFactorySimple.php
includes/libs/rdbms/loadbalancer/ILoadBalancer.php
includes/libs/rdbms/loadbalancer/LoadBalancer.php
includes/logging/BlockLogFormatter.php
includes/mail/EmailNotification.php
includes/mail/UserMailer.php
includes/objectcache/ObjectCache.php
includes/page/Article.php
includes/page/ImagePage.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/parser/RemexStripTagHandler.php
includes/profiler/Profiler.php
includes/profiler/SectionProfiler.php
includes/rcfeed/MachineReadableRCFeedFormatter.php
includes/registration/ExtensionProcessor.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderJqueryMsgModule.php [deleted file]
includes/resourceloader/ResourceLoaderLanguageNamesModule.php [deleted file]
includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php [deleted file]
includes/resourceloader/ResourceLoaderModule.php
includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php [deleted file]
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/resourceloader/ResourceLoaderUploadDialogModule.php [deleted file]
includes/resourceloader/ResourceLoaderWikiModule.php
includes/revisiondelete/RevDelLogItem.php
includes/revisiondelete/RevDelRevisionItem.php
includes/revisiondelete/RevisionDeleteUser.php
includes/search/SearchEngine.php
includes/session/SessionManager.php
includes/site/SiteSQLStore.php [deleted file]
includes/skins/BaseTemplate.php
includes/skins/QuickTemplate.php
includes/specialpage/ChangesListSpecialPage.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specialpage/QueryPage.php
includes/specials/SpecialBlock.php
includes/specials/SpecialEditTags.php
includes/specials/SpecialMediaStatistics.php
includes/specials/SpecialMovepage.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialRevisiondelete.php
includes/specials/SpecialTags.php
includes/specials/SpecialTrackingCategories.php
includes/specials/SpecialUndelete.php
includes/specials/SpecialUploadStash.php
includes/specials/SpecialUserrights.php
includes/specials/SpecialWatchlist.php
includes/specials/exception/SpecialUploadStashTooLargeException.php [new file with mode: 0644]
includes/specials/pagers/ActiveUsersPager.php
includes/templates/NoLocalSettings.mustache
includes/upload/UploadFromChunks.php
includes/upload/UploadStash.php
includes/upload/exception/UploadChunkFileException.php [new file with mode: 0644]
includes/upload/exception/UploadChunkVerificationException.php [new file with mode: 0644]
includes/upload/exception/UploadChunkZeroLengthFileException.php [new file with mode: 0644]
includes/upload/exception/UploadStashBadPathException.php [new file with mode: 0644]
includes/upload/exception/UploadStashException.php [new file with mode: 0644]
includes/upload/exception/UploadStashFileException.php [new file with mode: 0644]
includes/upload/exception/UploadStashFileNotFoundException.php [new file with mode: 0644]
includes/upload/exception/UploadStashNoSuchKeyException.php [new file with mode: 0644]
includes/upload/exception/UploadStashNotLoggedInException.php [new file with mode: 0644]
includes/upload/exception/UploadStashWrongOwnerException.php [new file with mode: 0644]
includes/upload/exception/UploadStashZeroLengthFileException.php [new file with mode: 0644]
includes/user/User.php
includes/watcheditem/NoWriteWatchedItemStore.php
includes/watcheditem/WatchedItemStore.php
languages/Language.php
languages/LanguageConverter.php
languages/data/CrhExceptions.php
languages/i18n/aeb-arab.json
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/arz.json
languages/i18n/azb.json
languages/i18n/ba.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bg.json
languages/i18n/bn.json
languages/i18n/bqi.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/cs.json
languages/i18n/da.json
languages/i18n/de.json
languages/i18n/diq.json
languages/i18n/en.json
languages/i18n/es.json
languages/i18n/et.json
languages/i18n/eu.json
languages/i18n/exif/ar.json
languages/i18n/exif/et.json
languages/i18n/exif/is.json
languages/i18n/exif/shi.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/gom-deva.json
languages/i18n/gom-latn.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/ia.json
languages/i18n/ig.json
languages/i18n/inh.json
languages/i18n/is.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/lrc.json
languages/i18n/lv.json
languages/i18n/mai.json
languages/i18n/mk.json
languages/i18n/mo.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/pt-br.json
languages/i18n/qqq.json
languages/i18n/roa-tara.json
languages/i18n/ru.json
languages/i18n/rue.json
languages/i18n/sat.json
languages/i18n/sh.json
languages/i18n/shi.json
languages/i18n/sr-ec.json
languages/i18n/sr-el.json
languages/i18n/tcy.json
languages/i18n/th.json
languages/i18n/tt-cyrl.json
languages/i18n/uk.json
languages/i18n/wa.json
languages/i18n/yue.json
languages/i18n/zh-hant.json
maintenance/Maintenance.php
maintenance/archives/patch-drop-comment-fields.sql [new file with mode: 0644]
maintenance/benchmarks/benchmarkPurge.php
maintenance/deleteBatch.php
maintenance/includes/DeleteLocalPasswords.php
maintenance/includes/MigrateActors.php [new file with mode: 0644]
maintenance/migrateActors.php
maintenance/migrateComments.php
maintenance/moveBatch.php
maintenance/mssql/archives/patch-drop-comment-fields.sql [new file with mode: 0644]
maintenance/mssql/tables.sql
maintenance/oracle/archives/patch-drop-comment-fields.sql [new file with mode: 0644]
maintenance/oracle/tables.sql
maintenance/populateArchiveRevId.php
maintenance/postgres/tables.sql
maintenance/resources/foreign-resources.yaml
maintenance/sqlite/archives/patch-archive-drop-ar_comment.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-filearchive-drop-fa_description.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-image-drop-img_description.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-ipblocks-drop-ipb_reason.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-logging-drop-log_comment.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-oldimage-drop-oi_description.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-protected_titles-drop-pt_reason.sql [new file with mode: 0644]
maintenance/sqlite/archives/patch-recentchanges-drop-rc_comment.sql [new file with mode: 0644]
maintenance/storage/compressOld.php
maintenance/tables.sql
maintenance/updateCredits.php
maintenance/userDupes.inc
package.json
resources/Resources.php
resources/lib/jquery.client/AUTHORS.txt
resources/lib/jquery.client/jquery.client.js
resources/lib/oojs-router/AUTHORS.txt
resources/lib/oojs-router/History.md [new file with mode: 0644]
resources/lib/oojs-router/LICENSE [new file with mode: 0644]
resources/lib/oojs-router/LICENSE-MIT [deleted file]
resources/lib/oojs-router/oojs-router.js
resources/src/jquery.tablesorter.styles/images/sort_both.png [new file with mode: 0644]
resources/src/jquery.tablesorter.styles/images/sort_both.svg [new file with mode: 0644]
resources/src/jquery.tablesorter.styles/images/sort_down.png [new file with mode: 0644]
resources/src/jquery.tablesorter.styles/images/sort_down.svg [new file with mode: 0644]
resources/src/jquery.tablesorter.styles/images/sort_up.png [new file with mode: 0644]
resources/src/jquery.tablesorter.styles/images/sort_up.svg [new file with mode: 0644]
resources/src/jquery.tablesorter.styles/jquery.tablesorter.styles.less [new file with mode: 0644]
resources/src/jquery.tablesorter/images/sort_both.png [deleted file]
resources/src/jquery.tablesorter/images/sort_both.svg [deleted file]
resources/src/jquery.tablesorter/images/sort_down.png [deleted file]
resources/src/jquery.tablesorter/images/sort_down.svg [deleted file]
resources/src/jquery.tablesorter/images/sort_up.png [deleted file]
resources/src/jquery.tablesorter/images/sort_up.svg [deleted file]
resources/src/jquery.tablesorter/jquery.tablesorter.less [deleted file]
resources/src/jquery/jquery.makeCollapsible.js
resources/src/jquery/jquery.suggestions.js
resources/src/mediawiki.ForeignStructuredUpload.BookletLayout/BookletLayout.js
resources/src/mediawiki.ForeignStructuredUpload.js
resources/src/mediawiki.action/images/nextredirect-ltr.svg
resources/src/mediawiki.action/images/nextredirect-rtl.svg
resources/src/mediawiki.action/images/redirect-ltr.svg
resources/src/mediawiki.action/images/redirect-rtl.svg
resources/src/mediawiki.feedlink/images/feed-icon.svg
resources/src/mediawiki.helplink/images/help.svg
resources/src/mediawiki.htmlform.styles/images/question.svg
resources/src/mediawiki.icon/images/arrow-collapsed-rtl.svg
resources/src/mediawiki.icon/images/arrow-expanded.svg
resources/src/mediawiki.inspect.js
resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js
resources/src/mediawiki.language/mediawiki.language.js
resources/src/mediawiki.language/mediawiki.language.names.js [new file with mode: 0644]
resources/src/mediawiki.language/mediawiki.language.specialCharacters.js [new file with mode: 0644]
resources/src/mediawiki.legacy/images/magnify-clip-ltr.svg
resources/src/mediawiki.legacy/images/magnify-clip-rtl.svg
resources/src/mediawiki.less/mediawiki.ui/variables.less
resources/src/mediawiki.libs.jpegmeta/export.js
resources/src/mediawiki.notification/notification.js
resources/src/mediawiki.pager.tablePager/images/arrow-sort-ascending.svg
resources/src/mediawiki.rcfilters/Controller.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/HighlightColors.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/UriProcessor.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/ChangesListViewModel.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/FilterGroup.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/FilterItem.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/FiltersViewModel.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/ItemModel.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/SavedQueriesModel.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/SavedQueryItemModel.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js [deleted file]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js [deleted file]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js [deleted file]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js [deleted file]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js [deleted file]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js [deleted file]
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js [deleted file]
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js [deleted file]
resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js [deleted file]
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js [deleted file]
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/mw.rcfilters.js
resources/src/mediawiki.rcfilters/ui/ChangesLimitAndDateButtonWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/ChangesLimitPopupWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/ChangesListWrapperWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/CheckboxInputWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/DatePopupWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterItemHighlightButton.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterMenuHeaderWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterMenuOptionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterMenuSectionOptionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterTagItemWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterTagMultiselectWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FilterWrapperWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/FormWrapperWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/GroupWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/HighlightColorPickerWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/HighlightPopupWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/ItemMenuOptionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/LiveUpdateButtonWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/MainWrapperWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/MarkSeenButtonWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/MenuSelectWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/RcTopSectionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/RclTargetPageWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/RclToOrFromWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/RclTopSectionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/SaveFiltersPopupButtonWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/SavedLinksListItemWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/SavedLinksListWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/TagItemWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/ValuePickerWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/ViewSwitchWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/WatchlistTopSectionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.GroupWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightPopupWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MainWrapperWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListItemWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ViewSwitchWidget.js [deleted file]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js [deleted file]
resources/src/mediawiki.skinning/images/audio-ltr.svg
resources/src/mediawiki.skinning/images/audio-rtl.svg
resources/src/mediawiki.skinning/images/chat-ltr.svg
resources/src/mediawiki.skinning/images/chat-rtl.svg
resources/src/mediawiki.skinning/images/document-ltr.svg
resources/src/mediawiki.skinning/images/document-rtl.svg
resources/src/mediawiki.skinning/images/external-ltr.svg
resources/src/mediawiki.skinning/images/external-rtl.svg
resources/src/mediawiki.skinning/images/ftp-ltr.svg
resources/src/mediawiki.skinning/images/ftp-rtl.svg
resources/src/mediawiki.skinning/images/magnify-clip-ltr.svg
resources/src/mediawiki.skinning/images/magnify-clip-rtl.svg
resources/src/mediawiki.skinning/images/mail.svg
resources/src/mediawiki.skinning/images/video.svg
resources/src/mediawiki.special.apisandbox/apisandbox.js
resources/src/mediawiki.special.block.js
resources/src/mediawiki.special.upload/upload.js
resources/src/mediawiki.special.userrights.js
resources/src/mediawiki.ui/components/buttons.less
resources/src/mediawiki.user.js
resources/src/mediawiki.util.js
resources/src/mediawiki.widgets/images/page-disambiguation-ltr.svg
resources/src/mediawiki.widgets/images/page-disambiguation-rtl.svg
resources/src/mediawiki.widgets/images/page-existing-ltr.svg
resources/src/mediawiki.widgets/images/page-existing-rtl.svg
resources/src/mediawiki.widgets/images/page-not-found-he-yi.svg
resources/src/mediawiki.widgets/images/page-not-found-ltr.svg
resources/src/mediawiki.widgets/images/page-not-found-rtl.svg
resources/src/mediawiki.widgets/images/page-redirect-ltr.svg
resources/src/mediawiki.widgets/images/page-redirect-rtl.svg
resources/src/startup/mediawiki.js
tests/parser/ParserTestRunner.php
tests/phpunit/LessFileCompilationTest.php
tests/phpunit/PHPUnit4And6Compat.php
tests/phpunit/data/media/translated.svg [new file with mode: 0644]
tests/phpunit/data/resourceloader/sample.json [new file with mode: 0644]
tests/phpunit/docs/ExportDemoTest.php
tests/phpunit/includes/ActorMigrationTest.php
tests/phpunit/includes/BlockTest.php
tests/phpunit/includes/CommentStoreTest.php
tests/phpunit/includes/CommentStoreTest.sql [new file with mode: 0644]
tests/phpunit/includes/EditPageTest.php
tests/phpunit/includes/MediaWikiTest.php
tests/phpunit/includes/MessageTest.php
tests/phpunit/includes/MovePageTest.php
tests/phpunit/includes/OutputPageTest.php
tests/phpunit/includes/Revision/MutableRevisionSlotsTest.php
tests/phpunit/includes/Revision/RenderedRevisionTest.php
tests/phpunit/includes/Revision/RevisionQueryInfoTest.php
tests/phpunit/includes/Revision/RevisionRecordTests.php
tests/phpunit/includes/Revision/RevisionRendererTest.php
tests/phpunit/includes/Revision/RevisionSlotsTest.php
tests/phpunit/includes/Revision/RevisionStoreDbTestBase.php
tests/phpunit/includes/Revision/RevisionStoreFactoryTest.php
tests/phpunit/includes/Revision/RevisionStoreTest.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
tests/phpunit/includes/Storage/PageUpdaterTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WikiMapTest.php
tests/phpunit/includes/XmlTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiComparePagesTest.php
tests/phpunit/includes/api/ApiErrorFormatterTest.php
tests/phpunit/includes/api/ApiMoveTest.php
tests/phpunit/includes/api/ApiQuerySiteinfoTest.php
tests/phpunit/includes/block/Restriction/PageRestrictionTest.php
tests/phpunit/includes/content/MessageContentTest.php
tests/phpunit/includes/content/TextContentTest.php
tests/phpunit/includes/content/WikitextContentHandlerTest.php
tests/phpunit/includes/content/WikitextContentTest.php
tests/phpunit/includes/db/DatabaseSqliteTest.php
tests/phpunit/includes/debug/logger/monolog/LogstashFormatterTest.php
tests/phpunit/includes/deferred/LinksUpdateTest.php
tests/phpunit/includes/htmlform/HTMLCheckMatrixTest.php
tests/phpunit/includes/import/ImportTest.php
tests/phpunit/includes/json/FormatJsonTest.php
tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php
tests/phpunit/includes/logging/DatabaseLogEntryTest.php
tests/phpunit/includes/media/JpegPixelFormatTest.php
tests/phpunit/includes/media/SvgHandlerTest.php
tests/phpunit/includes/page/ImagePageTest.php
tests/phpunit/includes/page/PageArchiveTestBase.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/parser/ParserOutputTest.php
tests/phpunit/includes/parser/TidyTest.php
tests/phpunit/includes/password/PasswordPolicyChecksTest.php
tests/phpunit/includes/registration/ExtensionProcessorTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderClientHtmlTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderFileModuleTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/phpunit/includes/search/SearchEnginePrefixTest.php
tests/phpunit/includes/search/SearchSuggestionSetTest.php
tests/phpunit/includes/site/MediaWikiSiteTest.php
tests/phpunit/includes/specials/SpecialBlockTest.php
tests/phpunit/includes/specials/SpecialSearchTest.php
tests/phpunit/includes/specials/pagers/BlockListPagerTest.php
tests/phpunit/includes/tidy/RemexDriverTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/maintenance/backupTextPassTest.php
tests/phpunit/maintenance/categoryChangesAsRdfTest.php [new file with mode: 0644]
tests/phpunit/maintenance/categoryChangesRdfTest.php [deleted file]
tests/phpunit/maintenance/deleteAutoPatrolLogsTest.php
tests/phpunit/maintenance/fetchTextTest.php
tests/phpunit/skins/SideBarTest.php
tests/phpunit/structure/ApiStructureTest.php
tests/phpunit/structure/DatabaseIntegrationTest.php
tests/phpunit/structure/SpecialPageFatalTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js
tests/selenium/wdio-mediawiki/RunJobs.js

index 3978c2a..99afa3b 100644 (file)
                <exclude name="MediaWiki.Commenting.IllegalSingleLineComment.IllegalSingleLineCommentStart" />
                <exclude name="MediaWiki.Commenting.IllegalSingleLineComment.IllegalSingleLineCommentEnd" />
                <exclude name="MediaWiki.ControlStructures.AssignmentInControlStructures.AssignmentInControlStructures" />
-               <exclude name="MediaWiki.Commenting.MissingCovers.MissingCovers" />
                <exclude name="MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName" />
                <exclude name="MediaWiki.Usage.DbrQueryUsage.DbrQueryFound" />
+               <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgAuth" />
+               <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgContLang" />
+               <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgParser" />
+               <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle" />
                <exclude name="MediaWiki.Usage.ForbiddenFunctions.passthru" />
-               <exclude name="MediaWiki.VariableAnalysis.ForbiddenGlobalVariables.ForbiddenGlobal$wgTitle" />
                <exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment" />
                <exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.SingleSpaceBeforeSingleLineComment" />
                <exclude name="Squiz.Scope.MethodScope.Missing" />
        <rule ref="MediaWiki.NamingConventions.PrefixedGlobalFunctions">
                <properties>
                        <!--
-                       includes/compat/normal/UtfNormalUtil.php
-                       * codepointToUtf8
-                       * escapeSingleString
-                       * hexSequenceToUtf8
-                       * utf8ToCodepoint
-                       * utf8ToHexSequence
                        includes/GlobalFunctions.php
                        * mimeTypeMatch
-                       maintenance/benchmarks/bench_strtr_str_replace.php
-                       * bfNormalizeTitleStrReplace
-                       * bfNormalizeTitleStrTr
                        maintenance/cdb.php
                        * cdbShowHelp
                        maintenance/language/transstat.php
@@ -55,7 +48,7 @@
                        tests/qunit/data/styleTest.css.php
                        * cssfilter
                        -->
-                       <property name="ignoreList" type="array" value="bfNormalizeTitleStrReplace,bfNormalizeTitleStrTr,cdbShowHelp,codepointToUtf8,compare_point,cssfilter,escapeSingleString,getEscapedProfileUrl,hexSequenceToUtf8,mccGetHelp,mccShowUsage,mimeTypeMatch,moveToExternal,NothingFunction,NothingFunctionData,resolveStub,resolveStubs,showUsage,utf8ToCodepoint,utf8ToHexSequence" />
+                       <property name="ignoreList" type="array" value="cdbShowHelp,compare_point,cssfilter,getEscapedProfileUrl,mccGetHelp,mccShowUsage,mimeTypeMatch,moveToExternal,NothingFunction,NothingFunctionData,resolveStub,resolveStubs,showUsage" />
                </properties>
        </rule>
        <rule ref="MediaWiki.NamingConventions.ValidGlobalName">
                        any new occurrences.
                -->
                <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
-               <exclude-pattern>*/includes/jobqueue/JobSpecification\.php</exclude-pattern>
                <exclude-pattern>*/includes/RevisionList\.php</exclude-pattern>
                <exclude-pattern>*/includes/installer/PhpBugTests\.php</exclude-pattern>
-               <exclude-pattern>*/includes/exception/LocalizedException\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/SpecialMostinterwikis\.php</exclude-pattern>
                <exclude-pattern>*/includes/cache/CacheDependency\.php</exclude-pattern>
-               <exclude-pattern>*/includes/cache/CacheHelper\.php</exclude-pattern>
                <exclude-pattern>*/includes/compat/XMPReader\.php</exclude-pattern>
                <exclude-pattern>*/includes/diff/DairikiDiff\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/SpecialAncientpages\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_Wikimedia_base_convert\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_delete_truncate\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_if_switch\.php</exclude-pattern>
-               <exclude-pattern>*/maintenance/benchmarks/bench_strtr_str_replace\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_utf8_title_check\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/benchmarks/bench_wfIsWindows\.php</exclude-pattern>
                <exclude-pattern>*/maintenance/cleanupTable.inc</exclude-pattern>
                <exclude-pattern>*/includes/api/ApiRsd\.php</exclude-pattern>
                <exclude-pattern>*/includes/AuthPlugin\.php</exclude-pattern>
                <exclude-pattern>*/includes/cache/CacheDependency\.php</exclude-pattern>
-               <exclude-pattern>*/includes/cache/CacheHelper\.php</exclude-pattern>
                <exclude-pattern>*/includes/compat/XMPReader\.php</exclude-pattern>
-               <exclude-pattern>*/includes/deferred/CdnCacheUpdate\.php</exclude-pattern>
                <exclude-pattern>*/includes/diff/DairikiDiff\.php</exclude-pattern>
                <exclude-pattern>*/includes/diff/DiffEngine\.php</exclude-pattern>
-               <exclude-pattern>*/includes/exception/LocalizedException\.php</exclude-pattern>
                <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
                <exclude-pattern>*/includes/filerepo/file/LocalFile\.php</exclude-pattern>
                <exclude-pattern>*/includes/gallery/PackedOverlayImageGallery\.php</exclude-pattern>
                <exclude-pattern>*/includes/HistoryBlob\.php</exclude-pattern>
                <exclude-pattern>*/includes/htmlform/HTMLFormElement\.php</exclude-pattern>
                <exclude-pattern>*/includes/jobqueue/aggregator/JobQueueAggregator\.php</exclude-pattern>
-               <exclude-pattern>*/includes/jobqueue/JobQueue\.php</exclude-pattern>
-               <exclude-pattern>*/includes/jobqueue/JobSpecification\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FileBackendStore\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FSFileBackend\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/SwiftFileBackend\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/forms/PreferencesFormLegacy\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/SpecialListusers\.php</exclude-pattern>
                <exclude-pattern>*/includes/specials/SpecialMyRedirectPages\.php</exclude-pattern>
-               <exclude-pattern>*/includes/specials/SpecialUploadStash\.php</exclude-pattern>
                <exclude-pattern>*/includes/StubObject\.php</exclude-pattern>
-               <exclude-pattern>*/includes/upload/UploadFromChunks\.php</exclude-pattern>
                <exclude-pattern>*/includes/upload/UploadStash\.php</exclude-pattern>
                <exclude-pattern>*/includes/utils/AutoloadGenerator\.php</exclude-pattern>
                <exclude-pattern>*/includes/WebResponse\.php</exclude-pattern>
index fec43f3..fdbf0ef 100644 (file)
@@ -13,6 +13,7 @@ module.exports = function ( grunt ) {
        grunt.loadNpmTasks( 'grunt-jsonlint' );
        grunt.loadNpmTasks( 'grunt-karma' );
        grunt.loadNpmTasks( 'grunt-stylelint' );
+       grunt.loadNpmTasks( 'grunt-svgmin' );
 
        karmaProxy[ wgScriptPath ] = {
                target: wgServer + wgScriptPath,
@@ -57,6 +58,41 @@ module.exports = function ( grunt ) {
                stylelint: {
                        src: '{resources/src,mw-config}/**/*.{css,less}'
                },
+               svgmin: {
+                       options: {
+                               js2svg: {
+                                       indent: '\t',
+                                       pretty: true
+                               },
+                               multipass: true,
+                               plugins: [ {
+                                       cleanupIDs: false
+                               }, {
+                                       removeDesc: false
+                               }, {
+                                       removeRasterImages: true
+                               }, {
+                                       removeTitle: false
+                               }, {
+                                       removeViewBox: false
+                               }, {
+                                       removeXMLProcInst: false
+                               }, {
+                                       sortAttrs: true
+                               } ]
+                       },
+                       all: {
+                               files: [ {
+                                       expand: true,
+                                       cwd: 'resources/src',
+                                       src: [
+                                               '**/*.svg'
+                                       ],
+                                       dest: 'resources/src/',
+                                       ext: '.svg'
+                               } ]
+                       }
+               },
                watch: {
                        files: [
                                '.{stylelintrc,eslintrc.json}',
@@ -120,9 +156,10 @@ module.exports = function ( grunt ) {
                return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH );
        } );
 
+       grunt.registerTask( 'minify', 'svgmin' );
        grunt.registerTask( 'lint', [ 'eslint', 'banana', 'stylelint' ] );
        grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] );
 
        grunt.registerTask( 'test', [ 'lint' ] );
-       grunt.registerTask( 'default', 'test' );
+       grunt.registerTask( 'default', [ 'minify', 'test' ] );
 };
index 24032bd..299cd94 100644 (file)
@@ -20,6 +20,8 @@ production.
   IP addresses, internationalized domain names, and possibly mailto links.
 * (T193868) $wgChangeTagsSchemaMigrationStage — This temporary setting, added in
   MediaWiki 1.32, now defaults to MIGRATION_NEW instead of MIGRATION_WRITE_BOTH.
+* Special:ActiveUsers will no longer filter out users who became inactive since
+  the last time the active users query cache was updated.
 
 ==== Removed configuration ====
 * (T199334) $wgTagStatisticsNewTable — This temporary setting, added in
@@ -33,6 +35,8 @@ production.
 * $wgEnableParserCache, deprecated since 1.26, was removed.
   If disabling the parser cache is still desirable,
   set `$wgParserCacheType = CACHE_NONE;` instead.
+* $wgCommentTableSchemaMigrationStage has been removed. Extension code finding
+  it unset should treat it as being MIGRATION_NEW.
 
 === New features in 1.33 ===
 * (T96041) __EXPECTUNUSEDCATEGORY__ on a category page causes the category
@@ -42,6 +46,10 @@ production.
   additional information about the authentication event.
 * TextContent::getText() was introduced as a replacement for
   Content::getNativeData() for text-based content models.
+* (T210814) SVGs are now by default displayed in wiki language on image
+  pages.
+* (T214706) LinksUpdate::getAddedExternalLinks() and
+  LinksUpdate::getRemovedExternalLinks() were introduced.
 
 === External library changes in 1.33 ===
 
@@ -51,12 +59,14 @@ production.
 
 ==== Changed external libraries ====
 * Updated OOUI from v0.29.2 to v0.30.2.
+* Updated OOjs Router from pre-release to v0.2.0.
 * Updated wikimedia/xmp-reader from 0.6.0 to 0.6.1.
 * Updated wikimedia/scoped-callback from 2.0.0 to 3.0.0.
 * Updated wikimedia/ip-set from 1.2.0 to 2.0.0.
   * The deprecated IPSet\IPSet alias was removed, Wikimedia\IPSet must be
     used instead.
 * Updated qunitjs from 2.6.2 to 2.9.1.
+* Updated jquery-client from 2.0.1 to 2.0.2.
 
 ==== Removed external libraries ====
 
@@ -182,6 +192,52 @@ because of Phabricator reports.
   mediawiki.api.messages, and mediawiki.api.rollback.
 * The 'jquery.byteLimit' module alias for 'jquery.lengthLimit',
   deprecated in 1.31, was removed.
+* Revision::fetchRevision(), deprecated in 1.28, was removed.
+* Class SquidUpdate, deprecated in 1.27, was removed.
+* Title->getSquidURLs(), deprecated in 1.27, was removed. Instead, use
+  Title->getCdnUrls().
+* Title::escapeFragmentForURL(), deprecated in 1.30, was removed. Use
+  Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki() instead.
+* Title->canTalk(), deprecated in 1.30, was removed. Instead, use
+  Title->canHaveTalkPage().
+* Title's methods for site and user page related to CSS and JS, deprecated in
+  1.31, were removed:
+  * Title->isCssOrJsPage() — Use Title->isSiteConfigPage()
+  * Title->isCssJsSubpage() – Use Title->isUserConfigPage()
+  * Title->getSkinFromCssJsSubpage() – Use Title->getSkinFromConfigSubpage()
+  * Title->isCssSubpage() – Use Title->isUserCssConfigPage()
+  * Title->isJsSubpage() – Use Title->isUserJsConfigPage()
+* SiteSQLStore, deprecated in 1.27 and whose only method, ::newInstance(),
+  would return the global SiteStore instance, has been removed. You can get to
+  this via MediaWiki\MediaWikiServices::getInstance()->getSiteStore() directly.
+* Linker::formatSize, deprecated in 1.28, has been removed (with DummyLinker's).
+  Instead, use Language->formatSize() with the relevant Language object.
+* Linker::formatTemplates, deprecated in 1.28, has been removed (along with the
+  version in DummyLinker). You can use TemplatesOnThisPageFormatter directly.
+* EventRelayerGroup::singleton(), deprecated in 1.27, has been removed. You can
+  use MediaWikiServices::getInstance()->getEventRelayerGroup() directly.
+* LinkCache->addLink(), deprecated in 1.27, has been removed. It is thought to
+  be unused, and is distinct from OutputPage->addLink(), which remains.
+* JsonContent->getJsonData(), deprecated in 1.25, has been removed. Instead, use
+  JsonContent->getData().
+* MWExceptionHandler::getLogId(), deprecated in 1.27, has been removed, as the
+  exception ID is the same as the request ID, from WebRequest::getRequestId().
+* SearchEngine::getNearMatchResultSet(), deprecated in 1.27, has been removed.
+  You can use SearchEngine::getNearMatcher() instead.
+* EmailNotification::updateWatchlistTimestamp, deprecated in 1.27, has been
+  removed. Instead, use WatchedItemStore::updateNotificationTimestamp directly.
+* User::getGroupName() and ::getGroupMember(), both deprecated in 1.29, have
+  been removed. Instead, please use UserGroupMembership::getGroupName() and
+  UserGroupMembership::getGroupMemberName().
+* Backwards compatibility for setting wgSessionsInObjectCache to false or using
+  wgSessionHandler, both of which were deprecated in 1.27 with the introduction
+  of SessionManager, has been removed.
+* SessionManager::autoCreateUser, deprecated in 1.27, has been removed. Use
+  MediaWiki\Auth\AuthManager::autoCreateUser instead.
+* The mw.libs.jpegmeta property, deprecated in 1.31, was removed.
+  Use require( 'mediawiki.libs.jpegmeta' ) instead.
+* The mw.user.stickyRandomId() method, deprecated in 1.32, was removed.
+  Use mw.user.getPageviewToken() instead.
 
 === Deprecations in 1.33 ===
 * The configuration option $wgUseESI has been deprecated, and is expected
@@ -219,11 +275,19 @@ because of Phabricator reports.
 * (T209699) The jquery.async module has been deprecated. JavaScript code that
   needs asynchronous behaviour should use Promises.
 * Password::equals() is deprecated, use verify().
+* BaseTemplate::msgWiki() and QuickTemplate::msgWiki() will be removed. Use
+  other means to fetch a properly escaped message string or Message object.
+* (T126091) The 'ResourceLoaderTestModules' hook, which lets you declare QUnit
+  testing code for your JavaScript modules, is deprecated. Instead, you can now
+  use the new extension registration key 'QUnitTestModule'.
+* (T213426) The jquery.throttle-debounce module has been deprecated. JavaScript
+  code that needs this behaviour should use OO.ui.debounce/throttle.
+* The mw.language.specialCharacters property from the
+  'mediawiki.language.specialCharacters' module has been deprecated.
+  Use require( 'mediawiki.language.specialCharacters' ) instead.
 
 === Other changes in 1.33 ===
-* (T208871) The hard-coded Google search form on the database error page was
-  removed.
-* (T201747) Html::openElement() warns if given an element name wiht a space
+* (T201747) Html::openElement() warns if given an element name with a space
   in it.
 
 == Compatibility ==
index 0d17916..d577272 100644 (file)
@@ -640,15 +640,15 @@ $wgAutoloadLocalClasses = [
        'HttpStatus' => __DIR__ . '/includes/libs/HttpStatus.php',
        'IApiMessage' => __DIR__ . '/includes/api/IApiMessage.php',
        'IBufferingStatsdDataFactory' => __DIR__ . '/includes/libs/stats/IBufferingStatsdDataFactory.php',
-       'ICacheHelper' => __DIR__ . '/includes/cache/CacheHelper.php',
+       'ICacheHelper' => __DIR__ . '/includes/cache/ICacheHelper.php',
        'IContextSource' => __DIR__ . '/includes/context/IContextSource.php',
        'IDBAccessObject' => __DIR__ . '/includes/dao/IDBAccessObject.php',
        'IDatabase' => __DIR__ . '/includes/libs/rdbms/database/IDatabase.php',
        'IEContentAnalyzer' => __DIR__ . '/includes/libs/mime/IEContentAnalyzer.php',
        'IEUrlExtension' => __DIR__ . '/includes/libs/IEUrlExtension.php',
        'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php',
-       'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
-       'ILocalizedException' => __DIR__ . '/includes/exception/LocalizedException.php',
+       'IJobSpecification' => __DIR__ . '/includes/jobqueue/IJobSpecification.php',
+       'ILocalizedException' => __DIR__ . '/includes/exception/ILocalizedException.php',
        'IMaintainableDatabase' => __DIR__ . '/includes/libs/rdbms/database/IMaintainableDatabase.php',
        'IP' => __DIR__ . '/includes/libs/IP.php',
        'IPTC' => __DIR__ . '/includes/media/IPTC.php',
@@ -708,14 +708,14 @@ $wgAutoloadLocalClasses = [
        'JobQueueAggregator' => __DIR__ . '/includes/jobqueue/aggregator/JobQueueAggregator.php',
        'JobQueueAggregatorNull' => __DIR__ . '/includes/jobqueue/aggregator/JobQueueAggregator.php',
        'JobQueueAggregatorRedis' => __DIR__ . '/includes/jobqueue/aggregator/JobQueueAggregatorRedis.php',
-       'JobQueueConnectionError' => __DIR__ . '/includes/jobqueue/JobQueue.php',
+       'JobQueueConnectionError' => __DIR__ . '/includes/jobqueue/exception/JobQueueConnectionError.php',
        'JobQueueDB' => __DIR__ . '/includes/jobqueue/JobQueueDB.php',
        'JobQueueEnqueueUpdate' => __DIR__ . '/includes/deferred/JobQueueEnqueueUpdate.php',
-       'JobQueueError' => __DIR__ . '/includes/jobqueue/JobQueue.php',
+       'JobQueueError' => __DIR__ . '/includes/jobqueue/exception/JobQueueError.php',
        'JobQueueFederated' => __DIR__ . '/includes/jobqueue/JobQueueFederated.php',
        'JobQueueGroup' => __DIR__ . '/includes/jobqueue/JobQueueGroup.php',
        'JobQueueMemory' => __DIR__ . '/includes/jobqueue/JobQueueMemory.php',
-       'JobQueueReadOnlyError' => __DIR__ . '/includes/jobqueue/JobQueue.php',
+       'JobQueueReadOnlyError' => __DIR__ . '/includes/jobqueue/exception/JobQueueReadOnlyError.php',
        'JobQueueRedis' => __DIR__ . '/includes/jobqueue/JobQueueRedis.php',
        'JobRunner' => __DIR__ . '/includes/jobqueue/JobRunner.php',
        'JobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
@@ -978,7 +978,7 @@ $wgAutoloadLocalClasses = [
        'MessageContent' => __DIR__ . '/includes/content/MessageContent.php',
        'MessageLocalizer' => __DIR__ . '/languages/MessageLocalizer.php',
        'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php',
-       'MigrateActors' => __DIR__ . '/maintenance/migrateActors.php',
+       'MigrateActors' => __DIR__ . '/maintenance/includes/MigrateActors.php',
        'MigrateArchiveText' => __DIR__ . '/maintenance/migrateArchiveText.php',
        'MigrateComments' => __DIR__ . '/maintenance/migrateComments.php',
        'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php',
@@ -1239,11 +1239,8 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderForeignApiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderForeignApiModule.php',
        'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php',
        'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php',
-       'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php',
        'ResourceLoaderLanguageDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageDataModule.php',
-       'ResourceLoaderLanguageNamesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageNamesModule.php',
        'ResourceLoaderLessVarFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLessVarFileModule.php',
-       'ResourceLoaderMediaWikiUtilModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php',
        'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php',
        'ResourceLoaderOOUIFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIFileModule.php',
        'ResourceLoaderOOUIImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderOOUIImageModule.php',
@@ -1251,9 +1248,7 @@ $wgAutoloadLocalClasses = [
        'ResourceLoaderSiteModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSiteModule.php',
        'ResourceLoaderSiteStylesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSiteStylesModule.php',
        'ResourceLoaderSkinModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSkinModule.php',
-       'ResourceLoaderSpecialCharacterDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php',
        'ResourceLoaderStartUpModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderStartUpModule.php',
-       'ResourceLoaderUploadDialogModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUploadDialogModule.php',
        'ResourceLoaderUserDefaultsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserDefaultsModule.php',
        'ResourceLoaderUserModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserModule.php',
        'ResourceLoaderUserOptionsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserOptionsModule.php',
@@ -1335,7 +1330,6 @@ $wgAutoloadLocalClasses = [
        'SiteImporter' => __DIR__ . '/includes/site/SiteImporter.php',
        'SiteList' => __DIR__ . '/includes/site/SiteList.php',
        'SiteLookup' => __DIR__ . '/includes/site/SiteLookup.php',
-       'SiteSQLStore' => __DIR__ . '/includes/site/SiteSQLStore.php',
        'SiteStats' => __DIR__ . '/includes/SiteStats.php',
        'SiteStatsInit' => __DIR__ . '/includes/SiteStatsInit.php',
        'SiteStatsUpdate' => __DIR__ . '/includes/deferred/SiteStatsUpdate.php',
@@ -1434,7 +1428,7 @@ $wgAutoloadLocalClasses = [
        'SpecialUnlockdb' => __DIR__ . '/includes/specials/SpecialUnlockdb.php',
        'SpecialUpload' => __DIR__ . '/includes/specials/SpecialUpload.php',
        'SpecialUploadStash' => __DIR__ . '/includes/specials/SpecialUploadStash.php',
-       'SpecialUploadStashTooLargeException' => __DIR__ . '/includes/specials/SpecialUploadStash.php',
+       'SpecialUploadStashTooLargeException' => __DIR__ . '/includes/specials/exception/SpecialUploadStashTooLargeException.php',
        'SpecialUserLogin' => __DIR__ . '/includes/specials/SpecialUserLogin.php',
        'SpecialUserLogout' => __DIR__ . '/includes/specials/SpecialUserLogout.php',
        'SpecialVersion' => __DIR__ . '/includes/specials/SpecialVersion.php',
@@ -1448,7 +1442,6 @@ $wgAutoloadLocalClasses = [
        'SqliteUpdater' => __DIR__ . '/includes/installer/SqliteUpdater.php',
        'SquidPurgeClient' => __DIR__ . '/includes/clientpool/SquidPurgeClient.php',
        'SquidPurgeClientPool' => __DIR__ . '/includes/clientpool/SquidPurgeClientPool.php',
-       'SquidUpdate' => __DIR__ . '/includes/deferred/CdnCacheUpdate.php',
        'SrConverter' => __DIR__ . '/languages/classes/LanguageSr.php',
        'StatsOutput' => __DIR__ . '/maintenance/language/StatOutputs.php',
        'StatsdAwareInterface' => __DIR__ . '/includes/libs/stats/StatsdAwareInterface.php',
@@ -1535,9 +1528,9 @@ $wgAutoloadLocalClasses = [
        'UpdateSearchIndex' => __DIR__ . '/maintenance/updateSearchIndex.php',
        'UpdateSpecialPages' => __DIR__ . '/maintenance/updateSpecialPages.php',
        'UploadBase' => __DIR__ . '/includes/upload/UploadBase.php',
-       'UploadChunkFileException' => __DIR__ . '/includes/upload/UploadFromChunks.php',
-       'UploadChunkVerificationException' => __DIR__ . '/includes/upload/UploadFromChunks.php',
-       'UploadChunkZeroLengthFileException' => __DIR__ . '/includes/upload/UploadFromChunks.php',
+       'UploadChunkFileException' => __DIR__ . '/includes/upload/exception/UploadChunkFileException.php',
+       'UploadChunkVerificationException' => __DIR__ . '/includes/upload/exception/UploadChunkVerificationException.php',
+       'UploadChunkZeroLengthFileException' => __DIR__ . '/includes/upload/exception/UploadChunkZeroLengthFileException.php',
        'UploadForm' => __DIR__ . '/includes/specials/forms/UploadForm.php',
        'UploadFromChunks' => __DIR__ . '/includes/upload/UploadFromChunks.php',
        'UploadFromFile' => __DIR__ . '/includes/upload/UploadFromFile.php',
@@ -1548,15 +1541,15 @@ $wgAutoloadLocalClasses = [
        'UploadSourceAdapter' => __DIR__ . '/includes/import/UploadSourceAdapter.php',
        'UploadSourceField' => __DIR__ . '/includes/specials/formfields/UploadSourceField.php',
        'UploadStash' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashBadPathException' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashException' => __DIR__ . '/includes/upload/UploadStash.php',
+       'UploadStashBadPathException' => __DIR__ . '/includes/upload/exception/UploadStashBadPathException.php',
+       'UploadStashException' => __DIR__ . '/includes/upload/exception/UploadStashException.php',
        'UploadStashFile' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashFileException' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashFileNotFoundException' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashNoSuchKeyException' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashNotLoggedInException' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/UploadStash.php',
-       'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/UploadStash.php',
+       'UploadStashFileException' => __DIR__ . '/includes/upload/exception/UploadStashFileException.php',
+       'UploadStashFileNotFoundException' => __DIR__ . '/includes/upload/exception/UploadStashFileNotFoundException.php',
+       'UploadStashNoSuchKeyException' => __DIR__ . '/includes/upload/exception/UploadStashNoSuchKeyException.php',
+       'UploadStashNotLoggedInException' => __DIR__ . '/includes/upload/exception/UploadStashNotLoggedInException.php',
+       'UploadStashWrongOwnerException' => __DIR__ . '/includes/upload/exception/UploadStashWrongOwnerException.php',
+       'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/exception/UploadStashZeroLengthFileException.php',
        'UppercaseCollation' => __DIR__ . '/includes/collation/UppercaseCollation.php',
        'User' => __DIR__ . '/includes/user/User.php',
        'UserArray' => __DIR__ . '/includes/user/UserArray.php',
index 845101d..fdc1730 100644 (file)
@@ -65,7 +65,7 @@
                "jakub-onderka/php-parallel-lint": "0.9.2",
                "jetbrains/phpstorm-stubs": "dev-master#38ff1a581b297f7901e961b8c923862ea80c3b96",
                "justinrainbow/json-schema": "~5.2",
-               "mediawiki/mediawiki-codesniffer": "23.0.0",
+               "mediawiki/mediawiki-codesniffer": "24.0.0",
                "monolog/monolog": "~1.22.1",
                "nikic/php-parser": "3.1.3",
                "seld/jsonlint": "1.7.1",
index dc0abdc..fcc41af 100644 (file)
                        "type": "object",
                        "description": "ResourceLoader sources to register"
                },
+               "QUnitTestModule": {
+                       "type": "object",
+                       "description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true."
+               },
                "ConfigRegistry": {
                        "type": "object",
                        "description": "Registry of factory functions to create Config objects"
index 9da636f..d561638 100644 (file)
                        "type": "object",
                        "description": "ResourceLoader sources to register"
                },
+               "QUnitTestModule": {
+                       "type": "object",
+                       "description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",
+                       "additionalProperties": false,
+                       "properties": {
+                               "localBasePath": {
+                                       "type": "string",
+                                       "description": "Prefix for local paths to files in $options, relative to extenion directory"
+                               },
+                               "remoteExtPath": {
+                                       "type": "string",
+                                       "description": "Prefix for URLs to files in $options, relative to $wgExtensionAssetsPath"
+                               },
+                               "remoteSkinPath": {
+                                       "type": "string",
+                                       "description": "Prefix for URLs to files in $options, relative to $wgStylePath"
+                               },
+                               "scripts": {
+                                       "type": ["string", "array"],
+                                       "description": "Scripts to include (array of file paths)",
+                                       "items": {
+                                               "type": "string"
+                                       }
+                               },
+                               "dependencies": {
+                                       "type": ["string", "array"],
+                                       "description": "Modules which must be loaded before this module",
+                                       "items": {
+                                               "type": "string"
+                                       }
+                               },
+                               "styles": {
+                                       "type": ["string", "array", "object"],
+                                       "description": "Styles to load",
+                                       "items": {
+                                               "type": "string"
+                                       }
+                               },
+                               "messages": {
+                                       "type": ["string", "array"],
+                                       "description": "Messages to load",
+                                       "items": {
+                                               "type": "string"
+                                       }
+                               }
+                       }
+               },
                "ConfigRegistry": {
                        "type": "object",
                        "description": "Registry of factory functions to create Config objects"
index d175bcd..8b5e4d7 100644 (file)
@@ -2833,17 +2833,17 @@ such as when responding to a resource
 loader request or generating HTML output.
 &$resourceLoader: ResourceLoader object
 
-'ResourceLoaderTestModules': Let you add new JavaScript testing modules. This is
-called after the addition of 'qunit' and MediaWiki testing resources.
-&$testModules: array of JavaScript testing modules. The 'qunit' framework,
-  included in core, is fed using tests/qunit/QUnitTestResources.php.
-  To add a new qunit module named 'myext.tests':
-       $testModules['qunit']['myext.tests'] = [
-               'script' => 'extension/myext/tests.js',
-               'dependencies' => <any module dependency you might have>
+'ResourceLoaderTestModules': DEPRECATED since 1.33! Register ResourceLoader modules
+that are only available when `$wgEnableJavaScriptTest` is true. Use this for test
+suites and other test-only resources.
+&$testModules: one array of modules per test framework. The modules array
+follows the same format as `$wgResourceModules`. For example:
+       $testModules['qunit']['ext.Example.test'] = [
+               'localBasePath' => __DIR__ . '/tests/qunit',
+               'remoteExtPath' => 'Example/tests/qunit',
+               'script' => [ 'tests/qunit/foo.js' ],
+               'dependencies' => [ 'ext.Example.foo' ]
         ];
-  For QUnit framework, the mediawiki.tests.qunit.testrunner dependency will be
-  added to any module.
 &$ResourceLoader: object
 
 'RevisionDataUpdates': Called when constructing a list of DeferrableUpdate to be
index 9dbc9eb..f8fbf83 100644 (file)
 require_once __DIR__ . '/../autoload.php';
 
 class AutoLoader {
-       static protected $autoloadLocalClassesLower = null;
+       protected static $autoloadLocalClassesLower = null;
 
        /**
         * @private Only public for ExtensionRegistry
         * @var string[] Namespace (ends with \) => Path (ends with /)
         */
-       static public $psr4Namespaces = [];
+       public static $psr4Namespaces = [];
 
        /**
         * autoload - take a class name and attempt to load it
index ea76cd6..85fa341 100644 (file)
@@ -727,6 +727,7 @@ class Block {
                        'ipb_create_account'   => $this->prevents( 'createaccount' ),
                        'ipb_deleted'          => (int)$this->mHideName, // typecast required for SQLite
                        'ipb_allow_usertalk'   => !$this->prevents( 'editownusertalk' ),
+                       'ipb_sitewide'         => $this->isSitewide(),
                ] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason )
                        + ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
        }
@@ -1185,6 +1186,9 @@ class Block {
                        case 'read':
                                $res = false;
                                break;
+                       case 'purge':
+                               $res = false;
+                               break;
                }
                if ( !$res && $blockDisablesLogin ) {
                        // If a block would disable login, then it should
index cba7a15..1a60bb7 100644 (file)
@@ -82,7 +82,11 @@ class CommentStore {
         */
        protected $key = null;
 
-       /** @var int One of the MIGRATION_* constants */
+       /**
+        * @var int One of the MIGRATION_* constants
+        * @todo Deprecate and remove once extensions seem unlikely to need to use
+        *  it for migration anymore.
+        */
        protected $stage;
 
        /** @var array[] Cache for `self::getJoin()` */
@@ -94,7 +98,8 @@ class CommentStore {
        /**
         * @param Language $lang Language to use for comment truncation. Defaults
         *  to content language.
-        * @param int $migrationStage One of the MIGRATION_* constants
+        * @param int $migrationStage One of the MIGRATION_* constants. Always
+        *  MIGRATION_NEW for MediaWiki core since 1.33.
         */
        public function __construct( Language $lang, $migrationStage ) {
                $this->stage = $migrationStage;
@@ -109,10 +114,10 @@ class CommentStore {
         * @return CommentStore
         */
        public static function newKey( $key ) {
-               global $wgCommentTableSchemaMigrationStage;
                wfDeprecated( __METHOD__, '1.31' );
-               $store = new CommentStore( MediaWikiServices::getInstance()->getContentLanguage(),
-                       $wgCommentTableSchemaMigrationStage );
+               $store = new CommentStore(
+                       MediaWikiServices::getInstance()->getContentLanguage(), MIGRATION_NEW
+               );
                $store->key = $key;
                return $store;
        }
index 00ccc96..dc8f1e8 100644 (file)
@@ -2512,21 +2512,11 @@ $wgMainStash = 'db-replicated';
  */
 $wgParserCacheExpireTime = 86400;
 
-/**
- * @deprecated since 1.27, session data is always stored in object cache.
- */
-$wgSessionsInObjectCache = true;
-
 /**
  * The expiry time to use for session storage, in seconds.
  */
 $wgObjectCacheSessionExpiry = 3600;
 
-/**
- * @deprecated since 1.27, MediaWiki\Session\SessionManager doesn't use PHP session storage.
- */
-$wgSessionHandler = null;
-
 /**
  * Whether to use PHP session handling ($_SESSION and session_*() functions)
  *
@@ -8964,13 +8954,6 @@ $wgExperiencedUserMemberSince = 30; # days
  */
 $wgInterwikiPrefixDisplayTypes = [];
 
-/**
- * Comment table schema migration stage.
- * @since 1.30
- * @var int One of the MIGRATION_* constants
- */
-$wgCommentTableSchemaMigrationStage = MIGRATION_NEW;
-
 /**
  * RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables).
  * Use the SCHEMA_COMPAT_XXX flags. Supported values:
@@ -9020,6 +9003,15 @@ $wgActorTableSchemaMigrationStage = SCHEMA_COMPAT_OLD;
  */
 $wgEnablePartialBlocks = false;
 
+/**
+ * Enable confirmation prompt for rollback actions to prevent accidental rollbacks.
+ * May be disabled to reduce number of clicks needed to perform rollbacks.
+ *
+ * @since 1.33
+ * @var bool
+ */
+$wgEnableRollbackConfirmationPrompt = true;
+
 /**
  * Enable stats monitoring when Block Notices are displayed in different places around core
  * and extensions.
index ba1233e..e46c45e 100644 (file)
@@ -403,38 +403,10 @@ class DummyLinker {
                );
        }
 
-       /**
-        * @deprecated since 1.28, use TemplatesOnThisPageFormatter directly
-        */
-       public function formatTemplates(
-               $templates,
-               $preview = false,
-               $section = false,
-               $more = null
-       ) {
-               wfDeprecated( __METHOD__, '1.28' );
-
-               return Linker::formatTemplates(
-                       $templates,
-                       $preview,
-                       $section,
-                       $more
-               );
-       }
-
        public function formatHiddenCategories( $hiddencats ) {
                return Linker::formatHiddenCategories( $hiddencats );
        }
 
-       /**
-        * @deprecated since 1.28, use Language::formatSize() directly
-        */
-       public function formatSize( $size ) {
-               wfDeprecated( __METHOD__, '1.28' );
-
-               return Linker::formatSize( $size );
-       }
-
        public function titleAttrib( $name, $options = null, array $msgParams = [] ) {
                return Linker::titleAttrib(
                        $name,
index 4599564..300c5f3 100644 (file)
@@ -3250,15 +3250,13 @@ ERROR;
         * @return array
         */
        private function getSummaryInputAttributes( array $inputAttrs = null ) {
-               $conf = $this->context->getConfig();
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
                // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-               // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+               // Unicode codepoints.
                return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
                        'id' => 'wpSummary',
                        'name' => 'wpSummary',
-                       'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                       'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                        'tabindex' => 1,
                        'size' => 60,
                        'spellcheck' => 'true',
index 95d11d9..091a5ca 100644 (file)
@@ -18,8 +18,6 @@
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
  * Factory class for spawning EventRelayer objects using configuration
  *
@@ -39,15 +37,6 @@ class EventRelayerGroup {
                $this->configByChannel = $config;
        }
 
-       /**
-        * @deprecated since 1.27 Use MediaWikiServices::getInstance()->getEventRelayerGroup()
-        * @return EventRelayerGroup
-        */
-       public static function singleton() {
-               wfDeprecated( __METHOD__, '1.27' );
-               return MediaWikiServices::getInstance()->getEventRelayerGroup();
-       }
-
        /**
         * @param string $channel
         * @return EventRelayer Relayer instance that handles the given channel
index 8866a88..5aa6edf 100644 (file)
@@ -246,9 +246,6 @@ class FileDeleteForm {
        private function showForm() {
                global $wgOut, $wgUser, $wgRequest;
 
-               $conf = RequestContext::getMain()->getConfig();
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
-
                $wgOut->addModules( 'mediawiki.action.delete.file' );
 
                $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $wgUser->isWatched( $this->title );
@@ -282,13 +279,13 @@ class FileDeleteForm {
 
                // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-               // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+               // Unicode codepoints.
                $fields[] = new OOUI\FieldLayout(
                        new OOUI\TextInputWidget( [
                                'name' => 'wpReason',
                                'inputId' => 'wpReason',
                                'tabIndex' => 2,
-                               'maxLength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                               'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                'infusable' => true,
                                'value' => $wgRequest->getText( 'wpReason' ),
                                'autofocus' => true,
index 363d7b8..e9e780d 100644 (file)
@@ -156,7 +156,7 @@ class GitInfo {
         * @return bool Whether or not the string looks like a SHA1
         */
        public static function isSHA1( $str ) {
-               return !!preg_match( '/^[0-9A-F]{40}$/i', $str );
+               return (bool)preg_match( '/^[0-9A-F]{40}$/i', $str );
        }
 
        /**
index a74060c..bd98932 100644 (file)
@@ -3123,7 +3123,7 @@ function wfIsBadImage( $name, $contextTitle = false, $blacklist = null ) {
 function wfCanIPUseHTTPS( $ip ) {
        $canDo = true;
        Hooks::run( 'CanIPUseHTTPS', [ $ip, &$canDo ] );
-       return !!$canDo;
+       return (bool)$canDo;
 }
 
 /**
index d1434f8..cc1df39 100644 (file)
@@ -1895,47 +1895,6 @@ class Linker {
                }
        }
 
-       /**
-        * @deprecated since 1.28, use TemplatesOnThisPageFormatter directly
-        *
-        * Returns HTML for the "templates used on this page" list.
-        *
-        * Make an HTML list of templates, and then add a "More..." link at
-        * the bottom. If $more is null, do not add a "More..." link. If $more
-        * is a Title, make a link to that title and use it. If $more is a string,
-        * directly paste it in as the link (escaping needs to be done manually).
-        * Finally, if $more is a Message, call toString().
-        *
-        * @since 1.16.3. $more added in 1.21
-        * @param Title[] $templates Array of templates
-        * @param bool $preview Whether this is for a preview
-        * @param bool $section Whether this is for a section edit
-        * @param Title|Message|string|null $more An escaped link for "More..." of the templates
-        * @return string HTML output
-        */
-       public static function formatTemplates( $templates, $preview = false,
-               $section = false, $more = null
-       ) {
-               wfDeprecated( __METHOD__, '1.28' );
-
-               $type = false;
-               if ( $preview ) {
-                       $type = 'preview';
-               } elseif ( $section ) {
-                       $type = 'section';
-               }
-
-               if ( $more instanceof Message ) {
-                       $more = $more->toString();
-               }
-
-               $formatter = new TemplatesOnThisPageFormatter(
-                       RequestContext::getMain(),
-                       MediaWikiServices::getInstance()->getLinkRenderer()
-               );
-               return $formatter->format( $templates, $type, $more );
-       }
-
        /**
         * Returns HTML for the "hidden categories on this page" list.
         *
@@ -1963,23 +1922,6 @@ class Linker {
                return $outText;
        }
 
-       /**
-        * @deprecated since 1.28, use Language::formatSize() directly
-        *
-        * Format a size in bytes for output, using an appropriate
-        * unit (B, KB, MB or GB) according to the magnitude in question
-        *
-        * @since 1.16.3
-        * @param int $size Size to format
-        * @return string
-        */
-       public static function formatSize( $size ) {
-               wfDeprecated( __METHOD__, '1.28' );
-
-               global $wgLang;
-               return htmlspecialchars( $wgLang->formatSize( $size ) );
-       }
-
        /**
         * Given the id of an interface element, constructs the appropriate title
         * attribute from the system messages.  (Note, this is usually the id but
index f5a954d..43512e1 100644 (file)
@@ -569,8 +569,11 @@ class MediaWiki {
        }
 
        /**
-        * This function commits all DB changes as needed before
-        * the user can receive a response (in case commit fails)
+        * This function commits all DB and session changes as needed *before* the
+        * client can receive a response (in case DB commit fails) and thus also before
+        * the response can trigger a subsequent related request by the client
+        *
+        * If there is a significant amount of content to flush, it can be done in $postCommitWork
         *
         * @param IContextSource $context
         * @param callable|null $postCommitWork [default: null]
@@ -598,6 +601,8 @@ class MediaWiki {
                // Run updates that need to block the user or affect output (this is the last chance)
                DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
                wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
+               // T214471: persist the session to avoid race conditions on subsequent requests
+               $request->getSession()->save();
 
                // Should the client return, their request should observe the new ChronologyProtector
                // DB positions. This request might be on a foreign wiki domain, so synchronously update
index 1919c0c..bcec0a1 100644 (file)
@@ -426,7 +426,7 @@ class MovePage {
         * Move a file associated with a page to a new location.
         * Can also be used to revert after a DB failure.
         *
-        * @access private
+        * @private
         * @param Title Old location to move the file from.
         * @param Title New location to move the file to.
         * @return Status
index bb8eba1..c677a09 100644 (file)
@@ -355,7 +355,6 @@ class ProtectionForm {
                $lang = $context->getLanguage();
                $conf = $context->getConfig();
                $cascadingRestrictionLevels = $conf->get( 'CascadingRestrictionLevels' );
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
                $out = '';
                if ( !$this->disabled ) {
                        $output->addModules( 'mediawiki.legacy.protect' );
@@ -502,10 +501,10 @@ class ProtectionForm {
 
                        // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                        // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-                       // Unicode codepoints (or 180 UTF-8 bytes for old schema).
+                       // Unicode codepoints.
                        // Subtract arbitrary 75 to leave some space for the autogenerated null edit's summary
                        // and other texts chosen by dropdown menus on this page.
-                       $maxlength = $oldCommentSchema ? 180 : CommentStore::COMMENT_CHARACTER_LIMIT - 75;
+                       $maxlength = CommentStore::COMMENT_CHARACTER_LIMIT - 75;
 
                        $out .= Xml::openElement( 'table', [ 'id' => 'mw-protect-table3' ] ) .
                                Xml::openElement( 'tbody' );
@@ -627,7 +626,7 @@ class ProtectionForm {
         * Show protection long extracts for this page
         *
         * @param OutputPage &$out
-        * @access private
+        * @private
         */
        function showLogExtract( &$out ) {
                # Show relevant lines from the protection log:
index c99f88a..aaf1069 100644 (file)
@@ -33,8 +33,6 @@ use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
-use Wikimedia\Rdbms\FakeResultWrapper;
 
 /**
  * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
@@ -119,7 +117,7 @@ class Revision implements IDBAccessObject {
         */
        public static function newFromId( $id, $flags = 0 ) {
                $rec = self::getRevisionLookup()->getRevisionById( $id, $flags );
-               return $rec === null ? null : new Revision( $rec, $flags );
+               return $rec ? new Revision( $rec, $flags ) : null;
        }
 
        /**
@@ -138,7 +136,7 @@ class Revision implements IDBAccessObject {
         */
        public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
                $rec = self::getRevisionLookup()->getRevisionByTitle( $linkTarget, $id, $flags );
-               return $rec === null ? null : new Revision( $rec, $flags );
+               return $rec ? new Revision( $rec, $flags ) : null;
        }
 
        /**
@@ -157,7 +155,7 @@ class Revision implements IDBAccessObject {
         */
        public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
                $rec = self::getRevisionLookup()->getRevisionByPageId( $pageId, $revId, $flags );
-               return $rec === null ? null : new Revision( $rec, $flags );
+               return $rec ? new Revision( $rec, $flags ) : null;
        }
 
        /**
@@ -244,7 +242,7 @@ class Revision implements IDBAccessObject {
        public static function loadFromId( $db, $id ) {
                wfDeprecated( __METHOD__, '1.31' ); // no known callers
                $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
-               return $rec === null ? null : new Revision( $rec );
+               return $rec ? new Revision( $rec ) : null;
        }
 
        /**
@@ -261,7 +259,7 @@ class Revision implements IDBAccessObject {
         */
        public static function loadFromPageId( $db, $pageid, $id = 0 ) {
                $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
-               return $rec === null ? null : new Revision( $rec );
+               return $rec ? new Revision( $rec ) : null;
        }
 
        /**
@@ -278,7 +276,7 @@ class Revision implements IDBAccessObject {
         */
        public static function loadFromTitle( $db, $title, $id = 0 ) {
                $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
-               return $rec === null ? null : new Revision( $rec );
+               return $rec ? new Revision( $rec ) : null;
        }
 
        /**
@@ -296,21 +294,7 @@ class Revision implements IDBAccessObject {
         */
        public static function loadFromTimestamp( $db, $title, $timestamp ) {
                $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
-               return $rec === null ? null : new Revision( $rec );
-       }
-
-       /**
-        * Return a wrapper for a series of database rows to
-        * fetch all of a given page's revisions in turn.
-        * Each row can be fed to the constructor to get objects.
-        *
-        * @param LinkTarget $title
-        * @return ResultWrapper
-        * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
-        */
-       public static function fetchRevision( LinkTarget $title ) {
-               wfDeprecated( __METHOD__, '1.31' );
-               return new FakeResultWrapper( [] );
+               return $rec ? new Revision( $rec ) : null;
        }
 
        /**
@@ -560,7 +544,7 @@ class Revision implements IDBAccessObject {
         * @param int $queryFlags
         * @param Title|null $title
         *
-        * @access private
+        * @private
         */
        function __construct( $row, $queryFlags = 0, Title $title = null ) {
                global $wgUser;
@@ -1028,7 +1012,7 @@ class Revision implements IDBAccessObject {
        public function getPrevious() {
                $title = $this->getTitle();
                $rec = self::getRevisionLookup()->getPreviousRevision( $this->mRecord, $title );
-               return $rec === null ? null : new Revision( $rec, self::READ_NORMAL, $title );
+               return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null;
        }
 
        /**
@@ -1039,7 +1023,7 @@ class Revision implements IDBAccessObject {
        public function getNext() {
                $title = $this->getTitle();
                $rec = self::getRevisionLookup()->getNextRevision( $this->mRecord, $title );
-               return $rec === null ? null : new Revision( $rec, self::READ_NORMAL, $title );
+               return $rec ? new Revision( $rec, self::READ_NORMAL, $title ) : null;
        }
 
        /**
index 1a7831b..95749c5 100644 (file)
@@ -62,7 +62,7 @@ abstract class RevisionRecord {
        protected $mWiki = false;
        /** @var int|null */
        protected $mId;
-       /** @var int|null */
+       /** @var int */
        protected $mPageId;
        /** @var UserIdentity|null */
        protected $mUser;
index 9a94389..44ca502 100644 (file)
@@ -85,7 +85,7 @@ return [
        'CommentStore' => function ( MediaWikiServices $services ) : CommentStore {
                return new CommentStore(
                        $services->getContentLanguage(),
-                       $services->getMainConfig()->get( 'CommentTableSchemaMigrationStage' )
+                       MIGRATION_NEW
                );
        },
 
index 23342e9..b4b6ce6 100644 (file)
@@ -67,6 +67,8 @@ require_once "$IP/includes/GlobalFunctions.php";
 // Load composer's autoloader if present
 if ( is_readable( "$IP/vendor/autoload.php" ) ) {
        require_once "$IP/vendor/autoload.php";
+} elseif ( file_exists( "$IP/vendor/autoload.php" ) ) {
+       die( "$IP/vendor/autoload.php exists but is not readable" );
 }
 
 // Assert that composer dependencies were successfully loaded
@@ -578,21 +580,6 @@ if ( $wgMaximalPasswordLength !== false ) {
        $wgPasswordPolicy['policies']['default']['MaximalPasswordLength'] = $wgMaximalPasswordLength;
 }
 
-// Backwards compatibility warning
-if ( !$wgSessionsInObjectCache ) {
-       wfDeprecated( '$wgSessionsInObjectCache = false', '1.27' );
-       if ( $wgSessionHandler ) {
-               wfDeprecated( '$wgSessionsHandler', '1.27' );
-       }
-       $cacheType = get_class( ObjectCache::getInstance( $wgSessionCacheType ) );
-       wfDebugLog(
-               'caches',
-               "Session data will be stored in \"$cacheType\" cache with " .
-                       "expiry $wgObjectCacheSessionExpiry seconds"
-       );
-}
-$wgSessionsInObjectCache = true;
-
 if ( $wgPHPSessionHandling !== 'enable' &&
        $wgPHPSessionHandling !== 'warn' &&
        $wgPHPSessionHandling !== 'disable'
index 6ada9b3..849707e 100644 (file)
@@ -38,7 +38,7 @@ use MediaWiki\MediaWikiServices;
  */
 class Title implements LinkTarget, IDBAccessObject {
        /** @var MapCacheLRU */
-       static private $titleCache = null;
+       private static $titleCache = null;
 
        /**
         * Title::newFromText maintains a cache to avoid expensive re-normalization of
@@ -205,7 +205,7 @@ class Title implements LinkTarget, IDBAccessObject {
        }
 
        /**
-        * @access protected
+        * @protected
         */
        function __construct() {
        }
@@ -767,23 +767,6 @@ class Title implements LinkTarget, IDBAccessObject {
                return $name;
        }
 
-       /**
-        * Escape a text fragment, say from a link, for a URL
-        *
-        * @deprecated since 1.30, use Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki()
-        *
-        * @param string $fragment Containing a URL or link fragment (after the "#")
-        * @return string Escaped string
-        */
-       static function escapeFragmentForURL( $fragment ) {
-               wfDeprecated( __METHOD__, '1.30' );
-               # Note that we don't urlencode the fragment.  urlencoded Unicode
-               # fragments appear not to work in IE (at least up to 7) or in at least
-               # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
-               # to care if they aren't encoded.
-               return Sanitizer::escapeId( $fragment, 'noninitial' );
-       }
-
        /**
         * Callback for usort() to do title sorts by (namespace, title)
         *
@@ -1071,17 +1054,6 @@ class Title implements LinkTarget, IDBAccessObject {
                        getNsText( MWNamespace::getTalk( $this->mNamespace ) );
        }
 
-       /**
-        * Can this title have a corresponding talk page?
-        *
-        * @deprecated since 1.30, use canHaveTalkPage() instead.
-        *
-        * @return bool True if this title either is a talk page or can have a talk page associated.
-        */
-       public function canTalk() {
-               return $this->canHaveTalkPage();
-       }
-
        /**
         * Can this title have a corresponding talk page?
         *
@@ -1308,17 +1280,6 @@ class Title implements LinkTarget, IDBAccessObject {
                );
        }
 
-       /**
-        * @return bool
-        * @deprecated Since 1.31; use ::isSiteConfigPage() instead (which also checks for JSON pages)
-        */
-       public function isCssOrJsPage() {
-               wfDeprecated( __METHOD__, '1.31' );
-               return ( NS_MEDIAWIKI == $this->mNamespace
-                               && ( $this->hasContentModel( CONTENT_MODEL_CSS )
-                                       || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
-       }
-
        /**
         * Is this a "config" (.css, .json, or .js) sub-page of a user page?
         *
@@ -1333,17 +1294,6 @@ class Title implements LinkTarget, IDBAccessObject {
                );
        }
 
-       /**
-        * @return bool
-        * @deprecated Since 1.31; use ::isUserConfigPage() instead (which also checks for JSON pages)
-        */
-       public function isCssJsSubpage() {
-               wfDeprecated( __METHOD__, '1.31' );
-               return ( NS_USER == $this->mNamespace && $this->isSubpage()
-                               && ( $this->hasContentModel( CONTENT_MODEL_CSS )
-                                       || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
-       }
-
        /**
         * Trim down a .css, .json, or .js subpage title to get the corresponding skin name
         *
@@ -1360,15 +1310,6 @@ class Title implements LinkTarget, IDBAccessObject {
                return substr( $subpage, 0, $lastdot );
        }
 
-       /**
-        * @deprecated Since 1.31; use ::getSkinFromConfigSubpage() instead
-        * @return string Containing skin name from .css, .json, or .js subpage title
-        */
-       public function getSkinFromCssJsSubpage() {
-               wfDeprecated( __METHOD__, '1.31' );
-               return $this->getSkinFromConfigSubpage();
-       }
-
        /**
         * Is this a CSS "config" sub-page of a user page?
         *
@@ -1383,15 +1324,6 @@ class Title implements LinkTarget, IDBAccessObject {
                );
        }
 
-       /**
-        * @deprecated Since 1.31; use ::isUserCssConfigPage()
-        * @return bool
-        */
-       public function isCssSubpage() {
-               wfDeprecated( __METHOD__, '1.31' );
-               return $this->isUserCssConfigPage();
-       }
-
        /**
         * Is this a JSON "config" sub-page of a user page?
         *
@@ -1420,15 +1352,6 @@ class Title implements LinkTarget, IDBAccessObject {
                );
        }
 
-       /**
-        * @deprecated Since 1.31; use ::isUserJsConfigPage()
-        * @return bool
-        */
-       public function isJsSubpage() {
-               wfDeprecated( __METHOD__, '1.31' );
-               return $this->isUserJsConfigPage();
-       }
-
        /**
         * Is this a sitewide CSS "config" page?
         *
@@ -1835,7 +1758,7 @@ class Title implements LinkTarget, IDBAccessObject {
         * @endcode
         *
         * @param string $text The subpage name to add to the title
-        * @return Title Subpage title
+        * @return Title|null Subpage title, or null on an error
         * @since 1.20
         */
        public function getSubpage( $text ) {
@@ -2703,10 +2626,11 @@ class Title implements LinkTarget, IDBAccessObject {
                }
 
                // Determine if the user is blocked from this action on this page.
-               // What gets passed into this method is a user right, not an action nmae.
+               // What gets passed into this method is a user right, not an action name.
                // There is no way to instantiate an action by restriction. However, this
                // will get the action where the restriction is the same. This may result
                // in actions being blocked that shouldn't be.
+               $actionObj = null;
                if ( Action::exists( $action ) ) {
                        // Clone the title to prevent mutations to this object which is done
                        // by Title::loadFromRow() in WikiPage::loadFromRow().
@@ -2714,14 +2638,16 @@ class Title implements LinkTarget, IDBAccessObject {
                        // Creating an action will perform several database queries to ensure that
                        // the action has not been overridden by the content type.
                        // @todo FIXME: Pass the relevant context into this function.
-                       $action = Action::factory( $action, $page, RequestContext::getMain() );
-               } else {
-                       $action = null;
+                       $actionObj = Action::factory( $action, $page, RequestContext::getMain() );
+                       // Ensure that the retrieved action matches the restriction.
+                       if ( $actionObj && $actionObj->getRestriction() !== $action ) {
+                               $actionObj = null;
+                       }
                }
 
                // If no action object is returned, assume that the action requires unblock
                // which is the default.
-               if ( !$action || $action->requiresUnblock() ) {
+               if ( !$actionObj || $actionObj->requiresUnblock() ) {
                        if ( $user->isBlockedFrom( $this, $useReplica ) ) {
                                // @todo FIXME: Pass the relevant context into this function.
                                $errors[] = $block
@@ -4017,13 +3943,6 @@ class Title implements LinkTarget, IDBAccessObject {
                return $urls;
        }
 
-       /**
-        * @deprecated since 1.27 use getCdnUrls()
-        */
-       public function getSquidURLs() {
-               return $this->getCdnUrls();
-       }
-
        /**
         * Purge all applicable CDN URLs
         */
index 3305f9f..628fbc0 100644 (file)
@@ -196,7 +196,7 @@ class WikiMap {
                                $infoMap = [];
                                // Make sure at least the current wiki is set, for simple configurations.
                                // This also makes it the first in the map, which is useful for common cases.
-                               $wikiId = self::getWikiIdFromDomain( self::getCurrentWikiDomain() );
+                               $wikiId = self::getWikiIdFromDbDomain( self::getCurrentWikiDbDomain() );
                                $infoMap[$wikiId] = [
                                        'url' => $wgCanonicalServer,
                                        'parts' => wfParseUrl( $wgCanonicalServer )
@@ -250,8 +250,9 @@ class WikiMap {
         *
         * @param string|DatabaseDomain $domain
         * @return string
+        * @since 1.31
         */
-       public static function getWikiIdFromDomain( $domain ) {
+       public static function getWikiIdFromDbDomain( $domain ) {
                $domain = DatabaseDomain::newFromId( $domain );
 
                if ( !in_array( $domain->getSchema(), [ null, 'mediawiki' ], true ) ) {
@@ -269,11 +270,20 @@ class WikiMap {
                        : (string)$domain->getDatabase();
        }
 
+       /**
+        * @param string $domain
+        * @return string
+        * @deprecated Since 1.33; use getWikiIdFromDbDomain()
+        */
+       public static function getWikiIdFromDomain( $domain ) {
+               return self::getWikiIdFromDbDomain( $domain );
+       }
+
        /**
         * @return DatabaseDomain Database domain of the current wiki
         * @since 1.33
         */
-       public static function getCurrentWikiDomain() {
+       public static function getCurrentWikiDbDomain() {
                global $wgDBname, $wgDBmwschema, $wgDBprefix;
                // Avoid invoking LBFactory to avoid any chance of recursion
                return new DatabaseDomain( $wgDBname, $wgDBmwschema, (string)$wgDBprefix );
@@ -284,9 +294,9 @@ class WikiMap {
         * @return bool Whether $domain has the same DB/prefix as the current wiki
         * @since 1.33
         */
-       public static function isCurrentWikiDomain( $domain ) {
+       public static function isCurrentWikiDbDomain( $domain ) {
                $domain = DatabaseDomain::newFromId( $domain );
-               $curDomain = self::getCurrentWikiDomain();
+               $curDomain = self::getCurrentWikiDbDomain();
 
                if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki' ], true ) ) {
                        // Include the schema if it is set and is not the default placeholder.
@@ -308,6 +318,6 @@ class WikiMap {
         * @since 1.33
         */
        public static function isCurrentWikiId( $wikiId ) {
-               return ( self::getWikiIdFromDomain( self::getCurrentWikiDomain() ) === $wikiId );
+               return ( self::getWikiIdFromDbDomain( self::getCurrentWikiDbDomain() ) === $wikiId );
        }
 }
index 1b90a1f..a796030 100644 (file)
@@ -32,7 +32,8 @@
  *
  * @note As of 1.21, XmlJsCode objects cannot be nested inside objects or arrays. The sole
  *       exception is the $args argument to Xml::encodeJsCall() because Xml::encodeJsVar() is
- *       called for each individual element in that array.
+ *       called for each individual element in that array. If you need to encode an object or array
+ *       containing XmlJsCode objects, use XmlJsCode::encodeObject() to re-encode it first.
  *
  * @since 1.17
  */
@@ -42,4 +43,33 @@ class XmlJsCode {
        function __construct( $value ) {
                $this->value = $value;
        }
+
+       /**
+        * Encode an object containing XmlJsCode objects.
+        *
+        * This takes an object or associative array where (some of) the values are XmlJsCode objects,
+        * and re-encodes it as a single XmlJsCode object.
+        *
+        * @since 1.33
+        * @param object|array $obj Object or associative array to encode
+        * @param bool $pretty If true, add non-significant whitespace to improve readability.
+        * @return XmlJsCode
+        */
+       public static function encodeObject( $obj, $pretty = false ) {
+               $parts = [];
+               foreach ( $obj as $key => $value ) {
+                       $parts[] =
+                               ( $pretty ? '    ' : '' ) .
+                               Xml::encodeJsVar( $key, $pretty ) .
+                               ( $pretty ? ': ' : ':' ) .
+                               Xml::encodeJsVar( $value, $pretty );
+               }
+               return new self(
+                       '{' .
+                       ( $pretty ? "\n" : '' ) .
+                       implode( $pretty ? ",\n" : ',', $parts ) .
+                       ( $pretty ? "\n" : '' ) .
+                       '}'
+               );
+       }
 }
index b60820c..47bbdc0 100644 (file)
@@ -358,8 +358,6 @@ class McrUndoAction extends FormAction {
 
        protected function getFormFields() {
                $request = $this->getRequest();
-               $config = $this->context->getConfig();
-               $oldCommentSchema = $config->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
                $ret = [
                        'diff' => [
                                'type' => 'info',
@@ -375,7 +373,7 @@ class McrUndoAction extends FormAction {
                                'name' => 'wpSummary',
                                'cssclass' => 'mw-summary',
                                'label-message' => 'summary',
-                               'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                               'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                'value' => $request->getVal( 'wpSummary', '' ),
                                'size' => 60,
                                'spellcheck' => 'true',
index 1efd747..21e20c2 100644 (file)
@@ -1157,6 +1157,7 @@ abstract class ApiBase extends ContextSource {
                        }
 
                        $value = $this->getMain()->getCheck( $encParamName );
+                       $provided = $value;
                } elseif ( $type == 'upload' ) {
                        if ( isset( $default ) ) {
                                // Having a default value is not allowed
@@ -1169,6 +1170,7 @@ abstract class ApiBase extends ContextSource {
                                self::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
                        }
                        $value = $this->getMain()->getUpload( $encParamName );
+                       $provided = $value->exists();
                        if ( !$value->exists() ) {
                                // This will get the value without trying to normalize it
                                // (because trying to normalize a large binary file
@@ -1183,6 +1185,7 @@ abstract class ApiBase extends ContextSource {
                        }
                } else {
                        $value = $this->getMain()->getVal( $encParamName, $default );
+                       $provided = $this->getMain()->getCheck( $encParamName );
 
                        if ( isset( $value ) && $type == 'namespace' ) {
                                $type = MWNamespace::getValidNamespaces();
@@ -1373,7 +1376,7 @@ abstract class ApiBase extends ContextSource {
                        }
 
                        // Set a warning if a deprecated parameter has been passed
-                       if ( $deprecated && $value !== false ) {
+                       if ( $deprecated && $provided ) {
                                $feature = $encParamName;
                                $m = $this;
                                while ( !$m->isMain() ) {
@@ -1387,7 +1390,7 @@ abstract class ApiBase extends ContextSource {
                        }
 
                        // Set a warning if a deprecated parameter value has been passed
-                       $usedDeprecatedValues = $deprecatedValues && $value !== false
+                       $usedDeprecatedValues = $deprecatedValues && $provided
                                ? array_intersect( array_keys( $deprecatedValues ), (array)$value )
                                : [];
                        if ( $usedDeprecatedValues ) {
index 8a54c0b..33d971b 100644 (file)
@@ -118,7 +118,7 @@ class ApiQueryInfo extends ApiQueryBase {
                return $this->tokenFunctions;
        }
 
-       static protected $cachedTokens = [];
+       protected static $cachedTokens = [];
 
        /**
         * @deprecated since 1.24
index 7c6b463..779d601 100644 (file)
@@ -474,7 +474,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
         *
         * @param stdClass $row The row from which to extract the data.
         * @return array An array mapping strings (descriptors) to their respective string values.
-        * @access public
         */
        public function extractRowInfo( $row ) {
                /* Determine the title of the page that has been changed. */
index c636ba1..6e37774 100644 (file)
@@ -116,7 +116,6 @@ class ApiRevisionDelete extends ApiBase {
                }
 
                $list->reloadFromMaster();
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $item = $list->reset(); $list->current(); $item = $list->next() ) {
                        $data['items'][$item->getId()] += $item->getApiData( $this->getResult() );
                }
index 10016bc..b667382 100644 (file)
        "apihelp-createaccount-summary": "انشاء حساب مستخدم جديد",
        "apihelp-createaccount-param-preservestate": "إذا تم عرض <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> بشكل صحيح لـ<samp>hasprimarypreservedstate</samp>، فقد تم تعليم طلبات <samp>primary-required</samp> لكي يجب حذفها، إذا عرضت قيمة غير فارغة لـ<samp>preservedusername</samp> فيجب استخدام اسم المستخدم هذا للوسيط <var>username</var>.",
        "apihelp-createaccount-example-create": "بدء عملية إنشاء المستخدم <kbd>Example</kbd> بكلمة المرور <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "اسم المستخدم.",
-       "apihelp-createaccount-param-password": "كلمة المرور (يتم تجاهلها إذا تم تعيين <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "مجال للمصادقة الخارجية (اختياري).",
-       "apihelp-createaccount-param-token": "حصلت على رمز إنشاء حساب في الطلب الأول.",
-       "apihelp-createaccount-param-email": "عنوان البريد الإلكتروني للمستخدم (اختياري).",
-       "apihelp-createaccount-param-realname": "الاسم الحقيقي للمستخدم (اختياري).",
-       "apihelp-createaccount-param-mailpassword": "اذا تم تعيين اي قيمة, سيتم ارسال كلمة سر عشوائية للمستخدم عن طريق الاميل.",
-       "apihelp-createaccount-param-reason": "السبب اختياري لإنشاء الحساب لوضعه في السجلات.",
-       "apihelp-createaccount-param-language": "رمز اللغة لتعيينه كافتراضي للمستخدم (اختياري، لغة المحتوى الافتراضية).",
-       "apihelp-createaccount-example-pass": "إنشاء المستخدم <kbd>testuser</kbd> بكلمة المرور <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "إنشاء مستخدم <kbd>testmailuser</kbd> وأرسل كلمة المرور بالبريد الإلكتروني بشكل عشوائي.",
        "apihelp-cspreport-summary": "مستخدمة من قبل المتصفحات للإبلاغ عن انتهاكات سياسة أمن المحتوى. لا ينبغي أبدا أن تستخدم هذه الوحدة، إلا عند استخدامها تلقائيا باستخدام متصفح ويب CSP متوافق.",
        "apihelp-cspreport-param-reportonly": "علم على أنه تقرير عن سياسة الرصد، وليس فرض سياسة",
        "apihelp-cspreport-param-source": "ماذا أنشأ رأس CSP الذي تسبب في هذا التقرير",
index 99421fe..c401ec7 100644 (file)
        "apihelp-compare-param-torev": "Сағыштырыу өсөн икенсе версия.",
        "apihelp-compare-example-1": "1-се һәм 2-се версиялар араһында айырма эшләү",
        "apihelp-createaccount-summary": "Ҡатнашыусыларҙың яңы иҫәп яҙыуҙарын булдырыу.",
-       "apihelp-createaccount-param-name": "Ҡатнашыусы исеме.",
-       "apihelp-createaccount-param-password": "Серһүҙ (ignored if <var>$1mailpassword</var> is set).",
-       "apihelp-createaccount-param-domain": "Тышҡы аутентификация домены (өҫтәмә).",
-       "apihelp-createaccount-param-token": "Беренсе ғариза буйынса алынған токендың иҫәп яҙмаһын булдырыу",
-       "apihelp-createaccount-param-email": "Ҡатнашыусының электрон почта адресы (өҫтәмә).",
-       "apihelp-createaccount-param-realname": "Ҡатнашыусының ысын исеме(өҫтәмә)",
-       "apihelp-createaccount-param-mailpassword": "Әгәр ҙә теләһә ниндәй мәғәнә ҡуйылһа, осраҡлы серһүҙ ҡулланыусыға ебәреләсәк",
-       "apihelp-createaccount-param-reason": "Журналға яҙыу өсөн иҫәп яҙмаһын булдырыуға өҫтәмә сәбәп",
-       "apihelp-createaccount-param-language": "Тел кодын ҡулланыусы өсөн һүҙһеҙ ҡуйырға (мотлаҡ түгел, эсенә алғандағында тел  һүҙһеҙ файҙаланыла)",
-       "apihelp-createaccount-example-pass": "<kbd>test123</kbd> серһүҙле <kbd>testuser</kbd> ҡулланыусыһын булдырыу.",
-       "apihelp-createaccount-example-mail": "<kbd>testmailuser</kbd> ҡулланыусыһын һәм электрон почтаны булдырыу, осраҡлы серһеҙ яһау",
        "apihelp-delete-summary": "Битте юйырға.",
        "apihelp-delete-param-title": "Биттең баш һүҙен юйырға. <var>$1биттәрҙән</var> бергә файҙаланыу  мөмкин түгел.",
        "apihelp-delete-param-pageid": "Бит идентифакторы юйылыу өсөн биттәр.  <var>$1title</var> менән бергә ҡулланыла алмайҙар",
index 09e4106..dd69004 100644 (file)
        "apihelp-compare-param-torev": "Číslo revize druhé stránky k porovnání.",
        "apihelp-compare-example-1": "Porovnat revize 1 a 2.",
        "apihelp-createaccount-summary": "Vytvořit nový uživatelský účet.",
-       "apihelp-createaccount-param-name": "Uživatelské jméno.",
-       "apihelp-createaccount-param-password": "Heslo (ignorováno, pokud je nastaveno <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Doména pro externí ověření (volitelné).",
-       "apihelp-createaccount-param-email": "E-mailová adresa uživatele (nepovinné).",
-       "apihelp-createaccount-param-realname": "Skutečné jméno uživatele (nepovinné).",
-       "apihelp-createaccount-param-mailpassword": "Pokud je nastaveno na libovolnou hodnotu, zašle se náhodně vygenerované heslo na e-mail uživatele.",
-       "apihelp-createaccount-param-reason": "Případný důvod pro vytvoření účtu, který se zaznamená do logu.",
-       "apihelp-createaccount-param-language": "Kód jazyka, který se má uživateli nastavit jako výchozí (volitelné, výchozí je jazyk obsahu).",
-       "apihelp-createaccount-example-pass": "Vytvořit uživatele <kbd>testuser</kbd> s heslem <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Vytvořit uživatele <kbd>testmailuser</kbd> a zaslat mu e-mail s náhodně vygenerovaným heslem.",
        "apihelp-delete-summary": "Smazat stránku.",
        "apihelp-delete-param-title": "Název stránky, která se má smazat. Není možné použít společně s <var>$1pageid</var>.",
        "apihelp-delete-param-pageid": "ID stránky, která se má smazat. Není možné použít společně s <var>$1title</var>.",
index d5a5d64..4cad07f 100644 (file)
        "apihelp-compare-example-1": "Unterschied zwischen Version 1 und 2 abrufen",
        "apihelp-createaccount-summary": "Erstellt ein neues Benutzerkonto.",
        "apihelp-createaccount-param-preservestate": "Falls <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> für <samp>hasprimarypreservedstate</samp> wahr ausgegeben hat, sollten Anfragen, die als <samp>primary-required</samp> markiert wurden, ausgelassen werden. Falls ein nicht-leerer Wert für <samp>preservedusername</samp> zurückgegeben wurde, muss dieser Benutzername für den Parameter <var>username</var> verwendet werden.",
-       "apihelp-createaccount-param-name": "Benutzername.",
-       "apihelp-createaccount-param-password": "Passwort (wird ignoriert, wenn <var>$1mailpassword</var> angegeben ist).",
-       "apihelp-createaccount-param-domain": "Domain für die externe Authentifizierung (optional).",
-       "apihelp-createaccount-param-token": "Der in der ersten Anfrage erhaltene Benutzerkontenerstellungs-Token.",
-       "apihelp-createaccount-param-email": "E-Mail-Adresse des Benutzers (optional).",
-       "apihelp-createaccount-param-realname": "Realname des Benutzers (optional).",
-       "apihelp-createaccount-param-mailpassword": "Wenn ein Wert angegeben wird, wird ein zufälliges Passwort per E-Mail an den Benutzer versandt.",
-       "apihelp-createaccount-param-reason": "Optionale Begründung für die Benutzerkontenerstellung, die in den Logbüchern vermerkt wird.",
-       "apihelp-createaccount-param-language": "Festzulegender standardmäßiger Sprachcode für den Benutzer (optional, Standard ist Inhaltssprache).",
-       "apihelp-createaccount-example-pass": "Benutzer <kbd>testuser</kbd> mit dem Passwort <kbd>test123</kbd> erstellen.",
-       "apihelp-createaccount-example-mail": "Benutzer <kbd>testmailuser</kbd> erstellen und zufällig generiertes Passwort per E-Mail verschicken.",
        "apihelp-delete-summary": "Löscht eine Seite.",
        "apihelp-delete-param-title": "Titel der Seite, die gelöscht werden soll. Kann nicht zusammen mit <var>$1pageid</var> verwendet werden.",
        "apihelp-delete-param-pageid": "Seitennummer der Seite, die gelöscht werden soll. Kann nicht zusammen mit <var>$1title</var> verwendet werden.",
index 777c4e8..7204d27 100644 (file)
@@ -16,7 +16,6 @@
        "apihelp-clearhasmsg-summary": "Clears the <code>hasmsg</code> flag for the current user.",
        "apihelp-compare-summary": "Get the difference between 2 pages.",
        "apihelp-compare-extended-description": "A revision number, a page title, or a page ID for both \"from\" and \"to\" must be passed.",
-       "apihelp-createaccount-param-password": "Password (ignored if <var>$1mailpassword</var> is set).",
        "apihelp-delete-param-title": "Title of the page to delete. Cannot be used together with <var>$1pageid</var>.",
        "apihelp-delete-param-watch": "Add the page to the current user's watchlist.",
        "apihelp-delete-example-simple": "Delete <kbd>Main Page</kbd>.",
index 8258c30..25df749 100644 (file)
        "apihelp-createaccount-summary": "Create a new user account.",
        "apihelp-createaccount-param-preservestate": "If <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> returned true for <samp>hasprimarypreservedstate</samp>, requests marked as <samp>primary-required</samp> should be omitted. If it returned a non-empty value for <samp>preservedusername</samp>, that username must be used for the <var>username</var> parameter.",
        "apihelp-createaccount-example-create": "Start the process of creating user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Username.",
-       "apihelp-createaccount-param-password": "Password (ignored if <var>$1mailpassword</var> is set).",
-       "apihelp-createaccount-param-domain": "Domain for external authentication (optional).",
-       "apihelp-createaccount-param-token": "Account creation token obtained in first request.",
-       "apihelp-createaccount-param-email": "Email address of user (optional).",
-       "apihelp-createaccount-param-realname": "Real name of user (optional).",
-       "apihelp-createaccount-param-mailpassword": "If set to any value, a random password will be emailed to the user.",
-       "apihelp-createaccount-param-reason": "Optional reason for creating the account to be put in the logs.",
-       "apihelp-createaccount-param-language": "Language code to set as default for the user (optional, defaults to content language).",
-       "apihelp-createaccount-example-pass": "Create user <kbd>testuser</kbd> with password <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Create user <kbd>testmailuser</kbd> and email a randomly-generated password.",
 
        "apihelp-cspreport-summary": "Used by browsers to report violations of the Content Security Policy. This module should never be used, except when used automatically by a CSP compliant web browser.",
        "apihelp-cspreport-param-reportonly": "Mark as being a report from a monitoring policy, not an enforced policy",
index 26d09c3..6862e63 100644 (file)
        "apihelp-createaccount-summary": "Crear una nueva cuenta de usuario.",
        "apihelp-createaccount-param-preservestate": "Si <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> devolvió true (verdadero) para <samp>hasprimarypreservedstate</samp>, deberían omitirse las peticiones marcadas como <samp>primary-required</samp>. Si devolvió un valor no vacío para <samp>preservedusername</samp>, se debe usar ese nombre de usuario en el parámetro <var>username</var>.",
        "apihelp-createaccount-example-create": "Empezar el proceso de creación del usuario <kbd>Example</kbd> con la contraseña <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Nombre de usuario.",
-       "apihelp-createaccount-param-password": "Contraseña (ignorada si está establecido <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Dominio de autenticación externa (opcional).",
-       "apihelp-createaccount-param-token": "La clave de creación de cuenta se obtiene en la primera solicitud.",
-       "apihelp-createaccount-param-email": "Dirección de correo electrónico del usuario (opcional).",
-       "apihelp-createaccount-param-realname": "Nombre verdadero del usuario (opcional).",
-       "apihelp-createaccount-param-mailpassword": "Si está puesto cualquier valor se enviará una contraseña aleatoria al usuario.",
-       "apihelp-createaccount-param-reason": "Motivo opcional por el que crear una cuenta puesta en los registros.",
-       "apihelp-createaccount-param-language": "Código de idioma a establecer como predeterminado para el usuario (opcional, predeterminado al contenido del idioma).",
-       "apihelp-createaccount-example-pass": "Crear usuario <kbd>testuser</kbd> con la contraseña <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Crear usuario <kbd>testmailuser</kbd> y enviar una contraseña generada aleatoriamente.",
        "apihelp-cspreport-summary": "Utilizado por los navegadores para informar de violaciones a la normativa de seguridad de contenidos. Este módulo no debe usarse nunca, excepto cuando se usa automáticamente por un navegador web compatible con CSP.",
        "apihelp-cspreport-param-reportonly": "Marcar como informe proveniente de una normativa de vigilancia, no una impuesta",
        "apihelp-cspreport-param-source": "Qué generó la cabecera CSP que provocó este informe",
index f15e819..2f95348 100644 (file)
        "apihelp-compare-paramvalue-prop-size": "\"nondik\" eta \"nora\" berrikuspenen tamaina.",
        "apihelp-compare-example-1": "1. eta 2. berrikusketen arteko \"diff\"-a sortu.",
        "apihelp-createaccount-summary": "Erabiltzaile kontu berria sortu.",
-       "apihelp-createaccount-param-name": "Erabiltzaile izena.",
-       "apihelp-createaccount-param-domain": "Kanpoko autentifikaziorako domeinua (aukerakoa).",
-       "apihelp-createaccount-param-token": "Lehenengo eskaeran lortutako kontu sorrera token-a.",
-       "apihelp-createaccount-param-email": "Erabiltzailearen helbide elektronikoa (aukerakoa).",
-       "apihelp-createaccount-param-realname": "Erabiltzailearen benetako izena (aukerakoa).",
-       "apihelp-createaccount-param-mailpassword": "Edozein baliorako jarriz, erabiltzaileari mezu elektroniko baten bitartez ausazko pasahitza bidaliko zaio.",
-       "apihelp-createaccount-param-language": "Erabiltzailearentzako lehenetsiko den hizkuntza kodea (aukerakoa, edukien hizkuntza lehenetsia).",
        "apihelp-delete-summary": "Orrialde bat ezabatu.",
        "apihelp-delete-param-title": "Ezabatzeko orri izenburua. Hurrengoarekin batera ezin da erabili: <var>$1pageid</var>.",
        "apihelp-delete-param-pageid": "Ezabatzeko orri edo ID orria. Hurrengoarekin batera ezin da erabili: <var>$1title</var>.",
index a799d00..319b2d6 100644 (file)
        "apihelp-compare-paramvalue-prop-diffsize": "اندازهٔ تفاوت اچ‌تی‌ام‌ال، به بایت.",
        "apihelp-compare-example-1": "ایجاد تفاوت بین نسخه 1 و 2",
        "apihelp-createaccount-summary": "ایجاد حساب کاربری",
-       "apihelp-createaccount-param-name": "نام کاربری.",
-       "apihelp-createaccount-param-password": "رمز عبور (نادیده گرفته می‌شود اگر <var>$1mailpassword</var> تنظیم شده‌باشد).",
-       "apihelp-createaccount-param-domain": "دامنه برای احراز هویت خارجی (اختیاری).",
-       "apihelp-createaccount-param-email": "نشانی ایمیل کاربر (اختیاری)",
-       "apihelp-createaccount-param-realname": "نام واقعی کاربر (اختیاری).",
-       "apihelp-createaccount-param-mailpassword": "اگر به هر مقداری تنظیم شود، یک رمز عبور تصادفی به کاربر ایمیل خواهد شد.",
-       "apihelp-createaccount-param-reason": "دلیل اختیاری برای ایجاد حساب کاربری جهت قرارگرفتن در سیاهه‌ها.",
-       "apihelp-createaccount-example-pass": "ایجاد کاربر <kbd>testuser</kbd> همراه رمز عبور <kbd>test123</kbd>",
-       "apihelp-createaccount-example-mail": "ایجاد کاربر <kbd>testmailuser</kbd> و ارسال یک رمز عبور تصادفی به ای‌میل.",
        "apihelp-delete-summary": "حذف صفحه",
        "apihelp-delete-param-title": "عنوان صفحه‌ای که قصد حذفش را دارید. نمی‌تواند در کنار <var>$1pageid</var> استفاده شود.",
        "apihelp-delete-param-pageid": "شناسه صفحه‌ای که قصد حذفش را دارید. نمی‌تواند در کنار <var>$1title</var> استفاده شود.",
index c7f88a9..42e5a8b 100644 (file)
@@ -40,7 +40,7 @@
        "apihelp-main-param-maxlag": "La latence maximale peut être utilisée quand MédiaWiki est installé sur un cluster de base de données répliqué. Pour éviter des actions provoquant un supplément de latence de réplication de site, ce paramètre peut faire attendre le client jusqu’à ce que la latence de réplication soit inférieure à une valeur spécifiée. En cas de latence excessive, le code d’erreur <samp>maxlag</samp> est renvoyé avec un message tel que <samp>Attente de $host : $lag secondes de délai</samp>.<br />Voyez [[mw:Special:MyLanguage/Manual:Maxlag_parameter|Manuel: Maxlag parameter]] pour plus d’information.",
        "apihelp-main-param-smaxage": "Fixer l’entête HTTP de contrôle de cache <code>s-maxage</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.",
        "apihelp-main-param-maxage": "Fixer l’entête HTTP de contrôle de cache <code>max-age</code> à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.",
-       "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si la valeur est <kbd>user</kbd>, ou s’il a le droit d’un utilisateur robot si la valeur est <kbd>bot</kbd><!-- {{int:group-bot}} ? -->.",
+       "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si la valeur est <kbd>user</kbd>, ou s’il a le droit d’un utilisateur robot si la valeur est <kbd>bot</kbd>.",
        "apihelp-main-param-assertuser": "Vérifier que l’utilisateur actuel est l’utilisateur nommé.",
        "apihelp-main-param-requestid": "Toute valeur fournie ici sera incluse dans la réponse. Peut être utilisé pour distinguer des demandes.",
        "apihelp-main-param-servedby": "Inclure le nom d’hôte qui a renvoyé la requête dans les résultats.",
        "apihelp-createaccount-summary": "Créer un nouveau compte utilisateur.",
        "apihelp-createaccount-param-preservestate": "Si <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> a retourné true pour <samp>hasprimarypreservedstate</samp>, les demandes marquées comme <samp>primary-required</samp> doivent être omises. Si elle a retourné une valeur non vide pour <samp>preservedusername</samp>, ce nom d'utilisateur doit être utilisé pour le paramètre <var>username</var>.",
        "apihelp-createaccount-example-create": "Commencer le processus de création d’un utilisateur <kbd>Exemple</kbd> avec le mot de passe <kbd>ExempleMotDePasse</kbd>.",
-       "apihelp-createaccount-param-name": "Nom d’utilisateur.",
-       "apihelp-createaccount-param-password": "Mot de passe (ignoré si <var>$1mailpassword</var> est défini).",
-       "apihelp-createaccount-param-domain": "Domaine pour l’authentification externe (facultatif).",
-       "apihelp-createaccount-param-token": "Jeton de création de compte obtenu à la première requête.",
-       "apihelp-createaccount-param-email": "Adresse courriel de l’utilisateur (facultatif).",
-       "apihelp-createaccount-param-realname": "Vrai nom de l’utilisateur (facultatif).",
-       "apihelp-createaccount-param-mailpassword": "S’il est fixé à une valeur quelconque, un mot de passe aléatoire sera envoyé par courriel à l’utilisateur.",
-       "apihelp-createaccount-param-reason": "Motif facultatif de création du compte à mettre dans les journaux.",
-       "apihelp-createaccount-param-language": "Code de langue à mettre par défaut pour l’utilisateur (facultatif, par défaut langue du contenu).",
-       "apihelp-createaccount-example-pass": "Créer l’utilisateur <kbd>testuser</kbd> avec le mot de passe <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Créer l’utilisateur <kbd>testmailuser</kbd> et envoyer par courriel un mot de passe généré aléatoirement.",
        "apihelp-cspreport-summary": "Utilisé par les navigateurs pour signaler les violations de la politique de confidentialité du contenu. Ce module ne devrait jamais être utilisé, sauf quand il est utilisé automatiquement par un navigateur web compatible avec CSP.",
        "apihelp-cspreport-param-reportonly": "Marquer comme étant un rapport d’une politique de surveillance, et non une politique exigée",
        "apihelp-cspreport-param-source": "Ce qui a généré l’entête CSP qui a déclenché ce rapport",
        "apihelp-import-extended-description": "Noter que le POST HTTP doit être effectué comme un import de fichier (c’est-à-dire en utilisant multipart/form-data) lors de l’envoi d’un fichier pour le paramètre <var>xml</var>.",
        "apihelp-import-param-summary": "Résumé de l’importation de l’entrée de journal.",
        "apihelp-import-param-xml": "Fichier XML téléversé.",
-       "apihelp-import-param-interwikiprefix": "Pour les importations téléchargées : le préfixe interwiki à appliquer aux noms d’utilisateur inconnus (et aux utilisateurs connus si <var>$1assignknownusers</var> est positionné).",
+       "apihelp-import-param-interwikiprefix": "Pour les importations téléversées : le préfixe interwiki à appliquer aux noms d’utilisateurs inconnus (et aux utilisateurs connus si <var>$1assignknownusers</var> est positionné).",
        "apihelp-import-param-assignknownusers": "Affecter les modifications aux utilisateurs locaux quand l’utilisateur nommé existe localement.",
        "apihelp-import-param-interwikisource": "Pour les importations interwiki : wiki depuis lequel importer.",
        "apihelp-import-param-interwikipage": "Pour les importations interwiki : page à importer.",
        "apihelp-patrol-param-rcid": "ID de modification récente à patrouiller.",
        "apihelp-patrol-param-revid": "ID de révision à patrouiller.",
        "apihelp-patrol-param-tags": "Modifier les balises à appliquer à l’entrée dans le journal de surveillance.",
-       "apihelp-patrol-example-rcid": "Patrouiller une modification récente",
-       "apihelp-patrol-example-revid": "Patrouiller une révision",
+       "apihelp-patrol-example-rcid": "Patrouiller une modification récente.",
+       "apihelp-patrol-example-revid": "Patrouiller une révision.",
        "apihelp-protect-summary": "Modifier le niveau de protection d’une page.",
        "apihelp-protect-param-title": "Titre de la page à (dé)protéger. Impossible à utiliser avec $1pageid.",
        "apihelp-protect-param-pageid": "ID de la page à (dé)protéger. Impossible à utiliser avec $1title.",
        "apihelp-query+siteinfo-paramvalue-prop-usergroups": "Renvoie les groupes utilisateur et les droits associés.",
        "apihelp-query+siteinfo-paramvalue-prop-libraries": "Renvoie les bibliothèques installées sur le wiki.",
        "apihelp-query+siteinfo-paramvalue-prop-extensions": "Renvoie les extensions installées sur le wiki.",
-       "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Renvoie la liste des extensions de fichier (types de fichier) autorisées au téléversement.",
+       "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "Renvoie la liste des extensions de fichiers (types de fichiers) autorisées au téléversement.",
        "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "Renvoie l’information sur les droits du wiki (sa licence), si elle est disponible.",
        "apihelp-query+siteinfo-paramvalue-prop-restrictions": "Renvoie l’information sur les types de restriction disponibles (protection).",
        "apihelp-query+siteinfo-paramvalue-prop-languages": "Renvoie une liste des langues que MédiaWiki prend en charge (éventuellement localisée en utilisant <var>$1inlanguagecode</var>).",
        "apihelp-upload-param-async": "Faire les grosses opérations de fichiers de façon asynchrone quand c’est possible.",
        "apihelp-upload-param-checkstatus": "Récupérer uniquement l’état de téléversement pour la clé de fichier donnée.",
        "apihelp-upload-example-url": "Téléverser depuis une URL",
-       "apihelp-upload-example-filekey": "Terminer un téléversement qui a échoué à cause d’avertissements",
+       "apihelp-upload-example-filekey": "Terminer un téléversement qui a échoué à cause d’avertissements.",
        "apihelp-userrights-summary": "Modifier l’appartenance d’un utilisateur à un groupe.",
        "apihelp-userrights-param-user": "Nom d’utilisateur.",
        "apihelp-userrights-param-userid": "ID de l’utilisateur.",
        "apihelp-userrights-param-add": "Ajouter l’utilisateur à ces groupes, ou s’ils sont déjà membres, mettre à jour la date d’expiration de leur appartenance à ce groupe.",
        "apihelp-userrights-param-expiry": "Horodatages d’expiration. Peuvent être relatifs (par ex. <kbd>5 mois</kbd> ou <kbd>2 semaines</kbd>) ou absolus (par ex. <kbd>2014-09-18T12:34:56Z</kbd>). Si uniquement un horodatage est fixé, il sera utilisé pour tous les groupes passés au paramètre <var>$1add</var>. Utiliser <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, ou <kbd>never</kbd> pour une lien utilisateur-groupe qui n’expire jamais.",
        "apihelp-userrights-param-remove": "Supprimer l’utilisateur de ces groupes.",
-       "apihelp-userrights-param-reason": "Motif pour la modification.",
+       "apihelp-userrights-param-reason": "Motif de la modification.",
        "apihelp-userrights-param-tags": "Modifier les balises à appliquer à l’entrée dans le journal des droits utilisateur.",
-       "apihelp-userrights-example-user": "Ajouter l’utilisateur <kbd>FooBot</kbd> au groupe <kbd>bot</kbd><!-- {{int:group-bot}} ? -->, et le supprimer des groupes <kbd>sysop</kbd> et <kbd>bureaucrat</kbd>.",
-       "apihelp-userrights-example-userid": "Ajouter l’utilisateur d’ID <kbd>123</kbd> au groupe <kbd>robot</kbd>, et le supprimer des groupes <kbd>sysop</kbd> et <kbd>bureaucrate</kbd>.",
+       "apihelp-userrights-example-user": "Ajouter l’utilisateur <kbd>FooBot</kbd> au groupe <kbd>bot</kbd> et le supprimer des groupes <kbd>sysop</kbd> et <kbd>bureaucrat</kbd>.",
+       "apihelp-userrights-example-userid": "Ajouter l’utilisateur d’ID <kbd>123</kbd> au groupe <kbd>bot</kbd>, et le supprimer des groupes <kbd>sysop</kbd> et <kbd>bureaucrat</kbd>.",
        "apihelp-userrights-example-expiry": "Ajouter l'utilisateur <kbd>SometimeSysop</kbd> au groupe <kbd>sysop</kbd> pour 1 mois.",
        "apihelp-validatepassword-summary": "Valider un mot de passe conformément aux règles concernant les mots de passe du wiki.",
        "apihelp-validatepassword-extended-description": "La validation est <samp>Good</samp> si le mot de passe est acceptable, <samp>Change</samp> s'il peut être utilisé pour se connecter et doit être changé, ou  <samp>Invalid</samp> s'il n'est pas utilisable.",
        "apihelp-watch-param-unwatch": "Si défini, la page ne sera plus suivie plutôt que suivie.",
        "apihelp-watch-example-watch": "Suivre la page <kbd>Main Page</kbd>.",
        "apihelp-watch-example-unwatch": "Ne plus suivre la page <kbd>Page principale</kbd>.",
-       "apihelp-watch-example-generator": "Suivre les quelques premières pages de l’espace de nom principal",
+       "apihelp-watch-example-generator": "Suivre les premières pages de l’espace de noms principal.",
        "apihelp-format-example-generic": "Renvoyer le résultat de la requête dans le format $1.",
-       "apihelp-format-param-wrappedhtml": "Renvoyer le HTML avec une jolie mise en forme et les modules ResourceLoader associés comme un objet JSON.",
-       "apihelp-json-summary": "Extraire les données au format JSON.",
+       "apihelp-format-param-wrappedhtml": "Renvoyer le HTML avec une jolie mise en forme pour l'impression et les modules ResourceLoader associés comme un objet JSON.",
+       "apihelp-json-summary": "Sortir les données au format JSON.",
        "apihelp-json-param-callback": "Si spécifié, inclut la sortie dans l’appel d’une fonction fournie. Pour plus de sûreté, toutes les données spécifiques à l’utilisateur seront restreintes.",
-       "apihelp-json-param-utf8": "Si spécifié, encode la plupart (mais pas tous) des caractères non ASCII en URF-8 au lieu de les remplacer par leur séquence d’échappement hexadécimale. Valeur par défaut quand <var>formatversion</var> ne vaut pas <kbd>1</kbd>.",
-       "apihelp-json-param-ascii": "Si spécifié, encode toutes ses séquences d’échappement non ASCII utilisant l’hexadécimal. Valeur par défaut quand <var>formatversion</var> vaut <kbd>1</kbd>.",
+       "apihelp-json-param-utf8": "Si spécifié, encode la plupart des caractères non ASCII (mais pas tous) en UTF-8 au lieu de les remplacer par leur séquence d’échappement hexadécimale. Valeur par défaut quand <var>formatversion</var> ne vaut pas <kbd>1</kbd>.",
+       "apihelp-json-param-ascii": "Si spécifié, encode tous les caractères non ASCII en utilisant des séquences d’échappement hexadécimales. Valeur par défaut quand <var>formatversion</var> vaut <kbd>1</kbd>.",
        "apihelp-json-param-formatversion": "Mise en forme de sortie :\n;1:Format rétro-compatible (booléens de style XML, clés <samp>*</samp> pour les nœuds de contenu, etc.).\n;2:Format moderne.\n;latest:Utilise le dernier format (actuellement <kbd>2</kbd>), peut changer sans avertissement.",
-       "apihelp-jsonfm-summary": "Extraire les données au format JSON (affiché proprement en HTML).",
+       "apihelp-jsonfm-summary": "Extraire les données au format JSON (HTML mis en forme améliorée pour l'impression).",
        "apihelp-none-summary": "Ne rien extraire.",
        "apihelp-php-summary": "Extraire les données au format sérialisé de PHP.",
        "apihelp-php-param-formatversion": "Mise en forme de la sortie :\n;1:format rétro-compatible (booléens de style XML, clés <samp>*</samp> pour les nœuds de contenu, etc.).\n;2:format moderne.\n;latest:utilise le dernier format (actuellement <kbd>2</kbd>), peut changer sans avertissement.",
-       "apihelp-phpfm-summary": "Extraire les données au format sérialisé de PHP (affiché proprement en HTML).",
-       "apihelp-rawfm-summary": "Extraire les données, y compris les éléments de débogage, au format JSON (affiché proprement en HTML).",
+       "apihelp-phpfm-summary": "Extraire les données au format sérialisé de PHP (mise en forme HTML améliorée pour l'impression).",
+       "apihelp-rawfm-summary": "Extraire les données, y compris les éléments de débogage, au format JSON (mise en forme HTML améliorée pour l'impression).",
        "apihelp-xml-summary": "Extraire les données au format XML.",
        "apihelp-xml-param-xslt": "Si spécifié, ajoute la page nommée comme une feuille de style XSL. La valeur doit être un titre dans l’espace de noms {{ns:MediaWiki}} se terminant par <code>.xsl</code>.",
        "apihelp-xml-param-includexmlnamespace": "Si spécifié, ajoute un espace de noms XML.",
        "api-login-fail-badsessionprovider": "Impossible de se connecter en utilisant $1.",
        "api-login-fail-sameorigin": "Impossible de se connecter quand la politique de même origine n’est pas appliquée.",
        "api-pageset-param-titles": "Une liste des titres sur lesquels travailler.",
-       "api-pageset-param-pageids": "Une liste des IDs de page sur lesquelles travailler.",
-       "api-pageset-param-revids": "Une liste des IDs de révision sur lesquelles travailler.",
-       "api-pageset-param-generator": "Obtenir la liste des pages sur lesquelles travailler en exécutant le module de recherche spécifié.\n\n<strong>NOTE :<strong> les noms de paramètre du générateur doivent être préfixés avec un « g », voir les exemples.",
+       "api-pageset-param-pageids": "Une liste des IDs de pages sur lesquelles travailler.",
+       "api-pageset-param-revids": "Une liste des IDs de révisions sur lesquelles travailler.",
+       "api-pageset-param-generator": "Obtenir la liste des pages sur lesquelles travailler en exécutant le module de requête spécifié.\n\n<strong>NOTE :<strong> les noms de paramètre du générateur doivent être préfixés avec un « g », voir les exemples.",
        "api-pageset-param-redirects-generator": "Résoudre automatiquement les redirections dans <var>$1titles</var>, <var>$1pageids</var> et <var>$1revids</var>, et dans les pages renvoyées par <var>$1generator</var>.",
        "api-pageset-param-redirects-nogenerator": "Résoudre automatiquement les redirections dans <var>$1titles</var>, <var>$1pageids</var> et <var>$1revids</var>.",
-       "api-pageset-param-converttitles": "Convertir les titres dans d’autres variantes si nécessaire. Fonctionne uniquement si la langue de contenu du wiki prend en charge la conversion en variantes. Les langues qui prennent en charge la conversion en variante incluent $1.",
-       "api-help-title": "Aide de l’API de MediaWiki",
+       "api-pageset-param-converttitles": "Convertir les titres dans d’autres variantes si nécessaire. Fonctionne uniquement si la langue de contenu du wiki prend en charge la conversion en variantes. Les langues qui prennent en charge la conversion en variantes incluent $1.",
+       "api-help-title": "API aide de MediaWiki",
        "api-help-lead": "Ceci est une page d’aide de l’API de MediaWiki générée automatiquement.\n\nDocumentation et exemples : https://www.mediawiki.org/wiki/API",
        "api-help-main-header": "Module principal",
        "api-help-undocumented-module": "Aucune documentation pour le module $1.",
        "apierror-compare-relative-to-nothing": "Pas de révision 'depuis' pour <var>torelative</var> à laquelle se rapporter.",
        "apierror-contentserializationexception": "Échec de sérialisation du contenu : $1",
        "apierror-contenttoobig": "Le contenu que vous avez fourni dépasse la limite de taille d’un article, qui est de $1 {{PLURAL:$1|kilooctet|kilooctets}}.",
-       "apierror-copyuploadbaddomain": "Les téléversements par URL ne sont pas autorisés pour ce domaine.",
+       "apierror-copyuploadbaddomain": "Les téléversements par URL ne sont pas autorisés depuis ce domaine.",
        "apierror-copyuploadbadurl": "Les téléversements ne sont pas autorisés depuis cette URL.",
        "apierror-create-titleexists": "Les titres existants ne peuvent pas être protégés avec <kbd>create</kbd>.",
        "apierror-csp-report": "Erreur lors du traitement du rapport CSP: $1.",
        "apierror-exceptioncaught": "[$1] Exception interceptée: $2",
        "apierror-exceptioncaughttype": "[$1] Exception interceptée de type $2",
        "apierror-filedoesnotexist": "Le fichier n’existe pas.",
-       "apierror-fileexists-sharedrepo-perm": "Le fichier cible existe dans un dépôt partagé. Utilisr le paramètre <var>ignorewarnings</var> pour lécraser.",
+       "apierror-fileexists-sharedrepo-perm": "Le fichier cible existe dans un dépôt partagé. Utilisr le paramètre <var>ignorewarnings</var> pour le réécraser.",
        "apierror-filenopath": "Il n'est pas possible de récupérer le chemin du fichier local.",
        "apierror-filetypecannotberotated": "Le type du fichier ne peut pas être tourné.",
        "apierror-formatphp": "Cette réponse ne peut pas être représentée en utilisant <kbd>format=php</kbd>. Voir https://phabricator.wikimedia.org/T68776.",
        "apierror-invalidoldimage": "Le paramètre <var>oldimage</var> a un format non valide.",
        "apierror-invalidparammix-cannotusewith": "Le paramètre <kbd>$1</kbd> ne peut pas être utilisé avec <kbd>$2</kbd>.",
        "apierror-invalidparammix-mustusewith": "Le paramètre <kbd>$1</kbd> ne peut être utilisé qu’avec <kbd>$2</kbd>.",
-       "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> ne peut pas être combiné avec le paramètre <var>oldid</var>, <var>pageid</var> ou <var>page</var>. Veuillez utiliser <var>title</var> et <var>text</var>.",
+       "apierror-invalidparammix-parse-new-section": "<kbd>section=new</kbd> ne peut pas être combiné avec les paramètres <var>oldid</var>, <var>pageid</var> ou <var>page</var>. Veuillez utiliser <var>title</var> et <var>text</var>.",
        "apierror-invalidparammix": "{{PLURAL:$2|Les paramètres}} $1 ne peuvent pas être utilisés ensemble.",
        "apierror-invalidsection": "Le paramètre <var>section</var> doit être un ID de section valide ou <kbd>new</kbd>.",
        "apierror-invalidsha1base36hash": "Le hachage SHA1Base36 fourni n’est pas valide.",
        "apierror-maxbytes": "Le paramètre <var>$1</var> ne peut excéder $2 octets{{PLURAL:$2||s}}",
        "apierror-maxchars": "Le paramètre <var>$1</var> ne peut excéder $2 catactères{{PLURAL:$2||s}}",
        "apierror-maxlag-generic": "Attente d’un serveur de base de données : $1 {{PLURAL:$1|seconde|secondes}} de délai.",
-       "apierror-maxlag": "Attente de $2 : $1 {{PLURAL:$1|seconed|secondes}} de délai.",
+       "apierror-maxlag": "Attente de $2 : $1 {{PLURAL:$1|seconde|secondes}} de délai.",
        "apierror-mimesearchdisabled": "La recherche MIME est désactivée en mode Misère.",
        "apierror-missingcontent-pageid": "Contenu manquant pour la page d’ID $1.",
        "apierror-missingcontent-revid": "Contenu de la révision d’ID $1 manquant.",
        "apierror-missingrev-pageid": "Aucune révision actuelle de la page d’ID $1.",
        "apierror-missingrev-title": "Aucune révision actuelle de titre $1.",
        "apierror-missingtitle-createonly": "Les titres manquants ne peuvent être protégés qu’avec <kbd>create</kbd>.",
-       "apierror-missingtitle": "La page que vous avez spécifié n’existe pas.",
+       "apierror-missingtitle": "La page que vous avez spécifiée n’existe pas.",
        "apierror-missingtitle-byname": "La page $1 n’existe pas.",
        "apierror-moduledisabled": "Le module <kbd>$1</kbd> a été désactivé.",
        "apierror-multival-only-one-of": "{{PLURAL:$3|Seul|Seul un des}} $2 est autorisé pour le paramètre <var>$1</var>.",
        "apierror-no-direct-editing": "La modification directe via l’API n’est pas prise en charge pour le modèle de contenu $1 utilisé par $2.",
        "apierror-noedit-anon": "Les utilisateurs anonymes ne peuvent pas modifier les pages.",
        "apierror-noedit": "Vous n’avez pas le droit de modifier les pages.",
-       "apierror-noimageredirect-anon": "Les utilisateurs anonymes ne peut pas créer des redirections d’image.",
-       "apierror-noimageredirect": "Vous n’avez pas le droit de créer des redirections d’image.",
-       "apierror-nosuchlogid": "Il n’y a pas d’entrée du journal avec l’ID $1.",
+       "apierror-noimageredirect-anon": "Les utilisateurs anonymes ne peuvent pas créer des redirections d’images.",
+       "apierror-noimageredirect": "Vous n’avez pas le droit de créer des redirections d’images.",
+       "apierror-nosuchlogid": "Il n’y a pas d’entrée de journal avec l’ID $1.",
        "apierror-nosuchpageid": "Il n’y a pas de page avec l’ID $1.",
        "apierror-nosuchrcid": "Il n’y a pas de modification récente avec l’ID $1.",
        "apierror-nosuchrevid": "Il n’y a pas de révision d’ID $1.",
        "apierror-nosuchsection": "Il n’y a pas de section $1.",
        "apierror-nosuchsection-what": "Il ’y a pas de section $1 dans $2.",
        "apierror-nosuchuserid": "Il n'y a pas d'utilisateur ayant l'ID $1.",
-       "apierror-notarget": "Vous n’avez pas spécifié une cible valide pour cette action.",
+       "apierror-notarget": "Vous n’avez pas spécifié de cible valide pour cette action.",
        "apierror-notpatrollable": "La révision r$1 ne peut pas être patrouillée car elle est trop ancienne.",
        "apierror-nouploadmodule": "Aucun module de téléversement défini.",
        "apierror-offline": "Impossible de continuer du fait de problèmes de connexion au réseau. Assurez-vous d’avoir une connexion internet opérationnelle et réessayez.",
        "apierror-paramempty": "Le paramètre <var>$1</var> ne peut pas être vide.",
        "apierror-parsetree-notwikitext": "<kbd>prop=parsetree</kbd> n’est pris en charge que pour le contenu wikitexte.",
        "apierror-parsetree-notwikitext-title": "<kbd>prop=parsetree</kbd> n’est pris en charge que pour le contenu wikitexte. $1 utilise le modèle de contenu $2.",
-       "apierror-pastexpiry": "Le temps d’expiration « $1 » est dans le passé.",
+       "apierror-pastexpiry": "La date d’expiration « $1 » est dépassée.",
        "apierror-permissiondenied": "Vous n’avez pas le droit de $1.",
        "apierror-permissiondenied-generic": "Autorisation refusée.",
        "apierror-permissiondenied-patrolflag": "Vous avez besoin du droit <code>patrol</code> ou <code>patrolmarks</code> pour demander le drapeau patrouillé.",
        "apierror-ratelimited": "Vous avez dépassé votre limite de débit. Veuillez attendre un peu et réessayer.",
        "apierror-readapidenied": "Vous avez besoin du droit de lecture pour utiliser ce module.",
        "apierror-readonly": "Ce wiki est actuellement en mode lecture seule.",
-       "apierror-reauthenticate": "Vous n’avez pas authentifié récemment cette session ; veuillez vous authentifier de nouveau.",
+       "apierror-reauthenticate": "Vous ne vous êtes pas authentifié récemment pour cette session ; veuillez vous authentifier à nouveau.",
        "apierror-redirect-appendonly": "Vous avez essayé de modifier en utilisant le mode de suivi de redirection, qui doit être utilisé en lien avec <kbd>section=new</kbd>, <var>prependtext</var>, ou <var>appendtext</var>.",
-       "apierror-revdel-mutuallyexclusive": "Le même champ ne peut pas être utilisé à la fois en <var>hide</var> et <var>show</var>.",
+       "apierror-revdel-mutuallyexclusive": "Le même champ ne peut pas être utilisé à la fois pour <var>hide</var> et <var>show</var>.",
        "apierror-revdel-needtarget": "Un titre cible est nécessaire pour ce type RevDel.",
-       "apierror-revdel-paramneeded": "Au moins une valeur est nécessaire pour <var>hide</var> ou <var>show</var>.",
+       "apierror-revdel-paramneeded": "Au moins une valeur est nécessaire pour <var>hide</var> et/ou <var>show</var>.",
        "apierror-revisions-badid": "Pas de correction trouvée pour le paramètre <var>$1</var>.",
        "apierror-revisions-norevids": "Le paramètre <var>revids</var> ne peut pas être utilisé avec les options de liste (<var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var>, et <var>$1end</var>).",
        "apierror-revisions-singlepage": "<var>titles</var>, <var>pageids</var> ou un générateur a été utilisé pour fournir plusieurs pages, mais les paramètres <var>$1limit</var>, <var>$1startid</var>, <var>$1endid</var>, <kbd>$1dir=newer</kbd>, <var>$1user</var>, <var>$1excludeuser</var>, <var>$1start</var> et <var>$1end</var> ne peuvent être utilisés que sur une seule page.",
index 1217013..43e6e6d 100644 (file)
        "apihelp-createaccount-summary": "Crear unha nova conta de usuario.",
        "apihelp-createaccount-param-preservestate": "SE <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> devolve o valor \"certo\" para  <samp>hasprimarypreservedstate</samp>, as consultas marcadas como <samp>primary-required</samp> deben ser omitidas. Se devolve un valor non baleiro para <samp>preservedusername</samp>, ese nome de usuario debe usarse para o parámetro <var>username</var>.",
        "apihelp-createaccount-example-create": "Comezar o proceso de crear un usuario <kbd>Exemplo</kbd> con contrasinal <kbd>ExemploContrasinal</kbd>.",
-       "apihelp-createaccount-param-name": "Nome de usuario.",
-       "apihelp-createaccount-param-password": "Contrasinal (ignorado se <var>$1mailpassword</var> está activo)",
-       "apihelp-createaccount-param-domain": "Dominio para autenticación externa (opcional)",
-       "apihelp-createaccount-param-token": "Símbolo de creación de conta obtido á primeira.",
-       "apihelp-createaccount-param-email": "Enderezo de correo eletrónico do usuario (opcional).",
-       "apihelp-createaccount-param-realname": "Nome real do usuario (opcional).",
-       "apihelp-createaccount-param-mailpassword": "Se se establece calquera valor, enviarase un contrasinal aleatorio ao usuario.",
-       "apihelp-createaccount-param-reason": "Razón opcional de creación da conta para gardar nos rexistros.",
-       "apihelp-createaccount-param-language": "Código de lingua para usar como defecto polo usuario (de xeito opcional, usarase a lingua por defecto)",
-       "apihelp-createaccount-example-pass": "Crear usuario <kbd>testuser</kbd> con contrasinal <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Crear usuario <kbd>testmailuser</kbd>\"testmailuser\" e enviar por correo electrónico un contrasinal xenerado de forma aleatoria.",
        "apihelp-cspreport-summary": "Usado polos navegadores para informar de violacións da política de confidencialidade de contido. Este módulo non debe se usado nunca, excepto cando é usado automaticamente por un navegador web compatible con CSP.",
        "apihelp-cspreport-param-reportonly": "Marcar un informe dunha política de vixiancia e non unha política esixida",
        "apihelp-cspreport-param-source": "Que xerou a cabeceira CSP que lanzou este informe",
index ce0a877..04efe36 100644 (file)
        "apihelp-createaccount-summary": "יצירת חשבון משתמש חדש.",
        "apihelp-createaccount-param-preservestate": "אם <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> החזיר true עבור <samp>hasprimarypreservedstate</samp>, בקשות שמסומנות בתור <samp>primary-required</samp> אמורות להיות מושמטות. אם מוחזר ערך לא ריק ל־<samp>preservedusername</samp>, שם המשתמש הזה ישמש לפרמטר <var>username</var>.",
        "apihelp-createaccount-example-create": "תחילת תהליך יצירת המשתמש <kbd>Example</kbd> עם הססמה <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "שם משתמש.",
-       "apihelp-createaccount-param-password": "ססמה (לא ישפיע אם הוגדר <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "שם מתחם לאימות חיצוני (רשות).",
-       "apihelp-createaccount-param-token": "אסימון יצירת חשבון הושג בבקשה הראשונה.",
-       "apihelp-createaccount-param-email": "כתובת הדוא״ל של המשתמש (רשות).",
-       "apihelp-createaccount-param-realname": "השם האמתי של המשתמש (רשות).",
-       "apihelp-createaccount-param-mailpassword": "אם הוגדר ערך כלשהו, תישלח ססמה אקראית אל המשתמש.",
-       "apihelp-createaccount-param-reason": "הסיבה כרשות ליצירת החשבון כפי שתופיע ברישומים.",
-       "apihelp-createaccount-param-language": "קוד השפה שיוגדר כבררת המחדל למשתמש (רשות, בררת המחדל היא שפת התוכן).",
-       "apihelp-createaccount-example-pass": "יצירת המשתמש <kbd>testuser</kbd> עם הססמה <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "יצירת המשתמש <kbd>testmailuser</kbd> ושליחת ססמה שיוצרה אקראית בדוא״ל.",
        "apihelp-cspreport-summary": "משמש דפדפנים לדיווח הפרות של מדיניות אבטחת תוכן. המודול הזה לעולם לא ישמש אלא אם הוא משמש עם דפדפן תומך CSP.",
        "apihelp-cspreport-param-reportonly": "לסמן בתור דיווח ממדיניות מנטרת, לא מדיניות כפויה",
        "apihelp-cspreport-param-source": "מה ייצר את כותרת ה־CSP שייצרה את הדו״ח הזה",
index 4258674..59d7b28 100644 (file)
        "apihelp-compare-example-1": "Az 1-es és 2-es lapváltozat összehasonlítása.",
        "apihelp-createaccount-summary": "Új felhasználói fiók létrehozása.",
        "apihelp-createaccount-example-create": "<kbd>Example</kbd> felhasználói fiók létrehozásának elkezdése <kbd>ExamplePassword</kbd> jelszóval.",
-       "apihelp-createaccount-param-name": "Felhasználónév.",
-       "apihelp-createaccount-param-password": "Jelszó (figyelmen kívül hagyva, ha a <var>$1mailpassword</var> be van állítva).",
-       "apihelp-createaccount-param-domain": "Tartomány külső hitelesítéshez (opcionális).",
-       "apihelp-createaccount-param-token": "Felhasználólétrehozási token az első kérésből",
-       "apihelp-createaccount-param-email": "A szerkesztő e-mail-címe (nem kötelező).",
-       "apihelp-createaccount-param-realname": "A szerkesztő valódi neve (nem kötelező).",
-       "apihelp-createaccount-param-mailpassword": "Ha bármilyen értéket kap, egy véletlenszerű jelszót kap a felhasználó e-mailben.",
-       "apihelp-createaccount-param-reason": "Opcionális indoklás a fióklétrehozáshoz a naplókba.",
-       "apihelp-createaccount-param-language": "A felhasználó alapértelmezett nyelvkódja (opcionális, alapértelmezetten a tartalom nyelve).",
-       "apihelp-createaccount-example-pass": "<kbd>testuser</kbd> felhasználó létrehozása <kbd>test123</kbd> jelszóval.",
-       "apihelp-createaccount-example-mail": "<kbd>testmailuser</kbd> felhasználó létrehozása, véletlenszerű jelszó elküldése e-mailben.",
        "apihelp-delete-summary": "Lap törlése.",
        "apihelp-delete-param-title": "A törlendő lap címe. Nem használható együtt a <var>$1pageid</var> paraméterrel.",
        "apihelp-delete-param-pageid": "A törlendő lap lapazonosítója. Nem használható együtt a <var>$1title</var> paraméterrel.",
index d834337..8ffe98f 100644 (file)
@@ -16,7 +16,8 @@
                        "Matteocng",
                        "Einreiher",
                        "Mpiva",
-                       "Margherita.mignanelli"
+                       "Margherita.mignanelli",
+                       "Manfredi26"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Documentazione]] (in inglese)\n* [[mw:Special:MyLanguage/API:FAQ|FAQ]] (in inglese)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annunci sull'API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug & richieste]\n</div>\n<strong>Stato:</strong> l'API MediaWiki è un'interfaccia matura e stabile che è attivamente supportata e migliorata. Anche se cerchiamo di evitarlo, potremmo dover fare delle modifiche che causano malfunzionamenti; iscriviti alla [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mailing list sugli annunci delle API MediaWiki] per essere informato sugli aggiornamenti.\n\n<strong>Istruzioni sbagliate:</strong> quando vengono impartite alle API delle istruzioni sbagliate, un'intestazione HTTP verrà inviata col messaggio \"MediaWiki-API-Error\" e, sia il valore dell'intestazione, sia il codice d'errore, verranno impostati con lo stesso valore. Per maggiori informazioni leggi [[mw:Special:MyLanguage/API:Errors_and_warnings|API:Errori ed avvertimenti]] (in inglese).\n\n<p class=\"mw-apisandbox-link\"><strong>Test:</strong> per testare facilmente le richieste API, vedi [[Special:ApiSandbox]].</p>",
        "apihelp-createaccount-summary": "Crea un nuovo account utente.",
        "apihelp-createaccount-param-preservestate": "Se <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> ha restituito true per <samp>hasprimarypreservedstate</samp>, le richieste contrassegnate come <samp>primary-required</samp> dovrebbero essere omesse. Se invece ha restituito un valore non vuoto per <samp>preservedusername</samp>, quel nome utente deve essere utilizzato per il parametro <var>username</var>.",
        "apihelp-createaccount-example-create": "Avvia il processo di creazione utente <kbd>Example</kbd> con password <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Nome utente.",
-       "apihelp-createaccount-param-password": "Password (verrà ignorata se è impostato <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Dominio per l'autenticazione esterna (opzionale).",
-       "apihelp-createaccount-param-email": "Indirizzo Email dell'utente (opzionale).",
-       "apihelp-createaccount-param-realname": "Nome reale dell'utente (opzionale).",
-       "apihelp-createaccount-param-mailpassword": "Se impostato su un qualsiasi valore, una password random (casuale) verrà inviata all'utente.",
-       "apihelp-createaccount-param-reason": "Ragione, facoltativa, della creazione dell'account da inserire nei registri.",
-       "apihelp-createaccount-param-language": "Codice di lingua da impostare come predefinita per l'utente (opzionale, di default è la lingua del contenuto).",
-       "apihelp-createaccount-example-pass": "Crea l'utente <kbd>testuser</kbd> con password <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Crea l'utente <kbd>testmailuser</kbd> e mandagli via e-mail una password generata casualmente.",
        "apihelp-delete-summary": "Cancella una pagina.",
        "apihelp-delete-param-title": "Titolo della pagina che si desidera eliminare. Non può essere usato insieme a <var>$1pageid</var>.",
        "apihelp-delete-param-pageid": "ID di pagina della pagina da cancellare. Non può essere usato insieme con <var>$1title</var>.",
index bdda43a..4e22405 100644 (file)
        "apihelp-compare-example-1": "版1と2の差分を生成する。",
        "apihelp-createaccount-summary": "新しい利用者アカウントを作成します。",
        "apihelp-createaccount-example-create": "利用者 <kbd>Example</kbd> を作成する処理をパスワード <kbd>ExamplePassword</kbd> で開始する",
-       "apihelp-createaccount-param-name": "利用者名。",
-       "apihelp-createaccount-param-password": "パスワード (<var>$1mailpassword</var> が設定されると無視されます)。",
-       "apihelp-createaccount-param-domain": "外部認証のドメイン (省略可能)。",
-       "apihelp-createaccount-param-token": "最初のリクエストで得られたアカウント作成用トークンです。",
-       "apihelp-createaccount-param-email": "利用者の電子メールアドレス (任意)。",
-       "apihelp-createaccount-param-realname": "利用者の本名 (省略可能)。",
-       "apihelp-createaccount-param-mailpassword": "設定されると (その値を問わず)、無作為なパスワードがその利用者に電子メールで送られます。",
-       "apihelp-createaccount-param-reason": "ログに記録されるアカウント作成の理由 (任意)。",
-       "apihelp-createaccount-param-language": "利用者の言語コードの既定値 (省略可能, 既定ではコンテンツ言語)。",
-       "apihelp-createaccount-example-pass": "利用者 <kbd>testuser</kbd> をパスワード <kbd>test123</kbd> として作成する。",
-       "apihelp-createaccount-example-mail": "利用者 <kbd>testmailuser</kbd>を作成し、無作為に生成されたパスワードをメールで送る。",
        "apihelp-cspreport-param-source": "このレポートをトリガしたCSPヘッダを生成した内容",
        "apihelp-delete-summary": "ページを削除します。",
        "apihelp-delete-param-title": "削除するページ名です。<var>$1pageid</var> とは同時に使用できません。",
index 2598dc3..9497d8d 100644 (file)
        "apihelp-createaccount-summary": "새 사용자 계정을 만듭니다.",
        "apihelp-createaccount-param-preservestate": "<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>가 <samp>hasprimarypreservedstate</samp>에 대해 참을 반환하면 <samp>primary-required</samp>로 표시된 요청은 생략됩니다. <samp>preservedusername</samp>에 대해 비어있지 않은 값이 반환되면 해당 사용자 이름은 <var>username</var> 변수를 위해 사용됩니다.",
        "apihelp-createaccount-example-create": "비밀번호 <kbd>ExamplePassword</kbd>로 된 사용자 <kbd>Example</kbd>의 생성 과정을 시작합니다.",
-       "apihelp-createaccount-param-name": "사용자 이름",
-       "apihelp-createaccount-param-password": "비밀번호입니다. (<var>$1mailpassword</var>가 설정되어 있으면 무시됩니다)",
-       "apihelp-createaccount-param-domain": "외부 인증의 도메인 (선택적)",
-       "apihelp-createaccount-param-token": "첫 요청에서 획득한 계정 생성 토큰.",
-       "apihelp-createaccount-param-email": "사용자 이메일 주소 (선택).",
-       "apihelp-createaccount-param-realname": "사용자 실명 (선택).",
-       "apihelp-createaccount-param-mailpassword": "아무 값이든 존재한다면, 랜덤 비밀번호가 이메일로 전송됩니다.",
-       "apihelp-createaccount-param-reason": "선택적인, 기록에 남을 계정을 만드는 이유",
-       "apihelp-createaccount-param-language": "사용자에게 기본으로 설정할 언어 코드. (선택 사항, 기본값으로는 본문의 언어입니다)",
-       "apihelp-createaccount-example-pass": "사용자 <kbd>testuser</kbd>를 만들고 비밀번호를 <kbd>test123</kbd>으로 설정합니다.",
-       "apihelp-createaccount-example-mail": "사용자 <kbd>testmailuser</kbd>를 만들고 자동 생성된 비밀번호를 이메일로 보냅니다.",
        "apihelp-cspreport-summary": "브라우저가 콘텐츠 보안 정책의 위반을 보고하기 위해 사용합니다. 이 모듈은 SCP를 준수하는 웹 브라우저에 의해 자동으로 사용될 때를 제외하고는 사용해서는 안 됩니다.",
        "apihelp-cspreport-param-reportonly": "강제적 정책이 아닌, 모니터링 정책에서 나온 보고서인 것으로 표시합니다",
        "apihelp-cspreport-param-source": "이 보고서를 작동시킨 CSP 헤더를 생성한 원본입니다",
index 80d1416..669295c 100644 (file)
        "apihelp-compare-param-torev": "De Väsjohn vun dä zwaite Sigg zom verjlihsche.",
        "apihelp-compare-example-1": "Fengk de Ongerscheide zwesche dä Väsjohne 1 un 2",
        "apihelp-createaccount-summary": "Ene neue Zohjang för ene Metmaacher aanlähje.",
-       "apihelp-createaccount-param-name": "Der Nahme för dä Metmaacher.",
-       "apihelp-createaccount-param-password": "Et Paßwoot (Weed ävver it jebruc un övverjange, wann <code lang=\"en\" xml:lang=\"en\"><var>$1mailpassword</var></code> jesaz es)",
-       "apihelp-createaccount-param-domain": "De Domäijn för de Zohjangsdaht vun ußerhallef beschtähtech ze krijje. Kam_mer fott_lohße.",
-       "apihelp-createaccount-param-token": "Der Makkehrongsschlößel för ene Zohjang aanzelähje, dä mer bei de eezde Aanfrohch krääje hät.",
-       "apihelp-createaccount-param-email": "Däm Metmaacher sing Adräß för de <i lang=\"en\" xml:lang=\"en\">e-mail</i>, kann och fott bliive.",
-       "apihelp-createaccount-param-realname": "Dämm Metmaacher singe reeschtejje Nahme - kann fott blihve.",
-       "apihelp-createaccount-param-mailpassword": "Wann heh jädd aanjejovve es, kritt dä Metmaacher e zohfällesch ußjesöhk neu Paßwood aan sing Adräß för de <i lang=\"en\" xml:lang=\"en\">e-mail</i> jescheck.",
-       "apihelp-createaccount-param-reason": "Ene Jrond för dä Zojang aanzelähje, dä en de Logböhscher kütt.",
-       "apihelp-createaccount-param-language": "Dat Schprohcheköözel, wadd als der Schtandatt för dä Metmaacher jesaz wähde sull. Kann läddesch blihve, dann es et di Schprohch vum Wikki.",
-       "apihelp-createaccount-example-pass": "Lääsch dä Metmaacher <kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">testuser</kbd> aan, mem Paßwood <kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Lääsch dä Metmaacher <kbd lang=\"en\" xml:lang=\"en\" dir=\"ltr\">testmailuser</kbd> aan med emem zohfällesch ußjewörfelte Paßwoot un schegg_em dat övver de <i lang=\"en\" xml:lang=\"en\" dir=\"ltr\">e-mail</i>.",
        "apihelp-delete-summary": "Schmieß en Sigg fott.",
        "apihelp-delete-param-title": "De Övverschreff vun dä Sigg zom fottschmiiße. Kam_mer nit zersamme met „<var lang=\"en\" xml:lang=\"en\" dir=\"ltr\">$1pageid</var>“ bruche.",
        "apihelp-delete-param-pageid": "De Kännong vun dä Sigg zom fottschmiiße. Kam_mer nit zersamme met „<var lang=\"en\" xml:lang=\"en\" dir=\"ltr\">$1title</var>“ bruche.",
index 3f3c10a..615f71e 100644 (file)
@@ -20,9 +20,6 @@
        "apihelp-compare-param-totitle": "Zweeten Titel fir ze vergläichen.",
        "apihelp-compare-param-torev": "Zweet Versioun fir ze vergläichen.",
        "apihelp-createaccount-summary": "En neie Benotzerkont uleeën.",
-       "apihelp-createaccount-param-name": "Benotzernumm.",
-       "apihelp-createaccount-param-email": "E-Mail-Adress vum Benotzer (fakultativ).",
-       "apihelp-createaccount-param-realname": "Richtegen Numm vum Benotzer (fakultativ).",
        "apihelp-delete-summary": "Eng Säit läschen.",
        "apihelp-delete-param-watch": "D'Säit op dem aktuelle Benotzer seng Iwwerwaachungslëscht dobäisetzen.",
        "apihelp-delete-param-unwatch": "D'Säit vun der Iwwerwaachungslëscht vum aktuelle Benotzer erofhuelen.",
index 09b6671..f78c25c 100644 (file)
@@ -17,9 +17,6 @@
        "apihelp-compare-param-toid": "Antrojo lyginamo puslapio ID.",
        "apihelp-compare-param-prop": "Kokią informaciją gauti.",
        "apihelp-createaccount-summary": "Kurti naują vartotojo paskyrą.",
-       "apihelp-createaccount-param-name": "Naudotojo vardas.",
-       "apihelp-createaccount-param-email": "Vartotojo el. pašto adresas (nebūtina).",
-       "apihelp-createaccount-param-realname": "Vardas (nebūtina).",
        "apihelp-delete-summary": "Ištrinti puslapį.",
        "apihelp-delete-param-watch": "Pridėti puslapį prie dabartinio vartotojo stebimųjų sąrašo.",
        "apihelp-delete-param-unwatch": "Pašalinti puslapį iš dabartinio vartotojo stebimųjų sąrašo.",
index 14d508b..4bfea35 100644 (file)
        "apihelp-compare-param-torev": "Бтора преработка за споредба.",
        "apihelp-compare-example-1": "Дај разлика помеѓу преработките 1 и 2",
        "apihelp-createaccount-summary": "Создај нова корисничка сметка.",
-       "apihelp-createaccount-param-name": "Корисничко име.",
-       "apihelp-createaccount-param-password": "Лозинка (се занемарува ако е зададено <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Домен за надворешна заверка (незадолжително).",
-       "apihelp-createaccount-param-token": "Шифра за создавање сметка добиена во првото барање.",
-       "apihelp-createaccount-param-email": "Е-пошта на корисникот (незадолжително).",
-       "apihelp-createaccount-param-realname": "Вистинско име на корисникот (незадолжително).",
-       "apihelp-createaccount-param-mailpassword": "Ако му се зададе било каква вредност, тогаш на корисникот ќе му биде испратена случајна лозинка.",
-       "apihelp-createaccount-param-reason": "Незадолжителна прочина за создавање на сметката која ќе стои во дневниците.",
-       "apihelp-createaccount-param-language": "Јазичен код кој ќе биде стандарден за корисникот (незадолжително, по основно: јазикот на самото вики).",
-       "apihelp-createaccount-example-pass": "Создај го корисникот <kbd>testuser</kbd> со лозинката <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Создај го корисникот <kbd>testmailuser</kbd> и испрати случајно-создадена лозинка по е-пошта.",
        "apihelp-delete-summary": "Избриши страница.",
        "apihelp-delete-param-title": "Наслов на страницата што сакате да ја избришете. Не може да се користи заедно со <var>$1pageid</var>.",
        "apihelp-delete-param-pageid": "Назнака на страницата што сакате да ја избришете. Не може да се користи заедно со <var>$1title</var>.",
        "apihelp-help-param-submodules": "Прикажувај и помош за подмодули на именуваниот модул.",
        "apihelp-help-param-recursivesubmodules": "Прикажувај и помош за подмодули рекурзивно.",
        "apihelp-help-param-helpformat": "Формат на изводот на помошта.",
-       "apihelp-help-param-wrap": "Обвиткај го изводот како станрадна одѕивна структура од прилотот.",
+       "apihelp-help-param-wrap": "Обвиткај го изводот како стандардна одѕивна структура од прилотот.",
        "apihelp-help-param-toc": "Вклучи табела со содржина во HTML-изводот.",
        "apihelp-help-example-main": "Помош за главниот модул",
        "apihelp-help-example-submodules": "Помош за <kbd>action=query</kbd> и сите негови подмодули.",
index 3ba6228..7ac0575 100644 (file)
        "apihelp-createaccount-summary": "Opprett en ny brukerkonto.",
        "apihelp-createaccount-param-preservestate": "Om <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> returnerte true for <samp>hashprimarypreservedstate</samp> bør forespørsler merket som <samp>primary-required</samp> omgås. Om den returnerte en ikke-tom verdi for <samp>preservedusername</samp> kan det brukernavnet brukes for <var>username</var>-parameteren.",
        "apihelp-createaccount-example-create": "Start prosessen med å opprette brukeren <kbd>Example</kbd> med passordet <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Brukernavn.",
-       "apihelp-createaccount-param-password": "Passord (ignorert dersom <var>$1mailpassword</var> er satt).",
-       "apihelp-createaccount-param-domain": "Domene for ekstern autentisering (valgfritt).",
-       "apihelp-createaccount-param-token": "Kontoopprettingstegn som hentet i den første forespørselen.",
-       "apihelp-createaccount-param-email": "Brukerens e-postadresse (valgfritt).",
-       "apihelp-createaccount-param-realname": "Brukerens virkelige navn (valgfritt).",
-       "apihelp-createaccount-param-mailpassword": "Dersom satt til en verdi vil et tilfeldig passord bli sendt med e-post til brukeren.",
-       "apihelp-createaccount-param-reason": "Valgfri grunn for å opprette kontoen for å legges i loggene.",
-       "apihelp-createaccount-param-language": "Språkkode å bruke som standard for brukeren (valgfritt, standardverdien er innholdsspråket).",
-       "apihelp-createaccount-example-pass": "Opprett bruker <kbd>testuser</kbd> med passordet <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Opprett bruker <kbd>testmailuser</kbd> og send et tilfeldig generert passord med e-post.",
        "apihelp-cspreport-summary": "Brukes av nettlesere for å rapportere brudd på Content Security Policy. Denne modulen bør aldri brukes utenom av en CSP-mottakelig nettleser.",
        "apihelp-cspreport-param-source": "Hva som genererte CSP-headeren som utløste denne rapporten",
        "apihelp-delete-summary": "Slett en side.",
index c77d82d..84eef72 100644 (file)
        "apihelp-compare-param-torev": "Tweede te vergelijken revisie.",
        "apihelp-createaccount-summary": "Een nieuw gebruikersaccount aanmaken.",
        "apihelp-createaccount-example-create": "Start het proces voor het aanmaken van account <kbd>Example</kbd> met wachtwoord <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Gebruikersnaam.",
-       "apihelp-createaccount-param-password": "Wachtwoord (genegeerd als <var>$1mailpassword</var> is ingesteld).",
-       "apihelp-createaccount-param-domain": "Domein voor externe authentificatie (optioneel).",
-       "apihelp-createaccount-param-email": "E-mailadres van de gebruiker (optioneel).",
-       "apihelp-createaccount-param-realname": "Echte naam van de gebruiker (optioneel).",
-       "apihelp-createaccount-param-reason": "Optionele reden voor het aanmaken van het account, om in de logboeken te zetten.",
-       "apihelp-createaccount-param-language": "Taalcode om als standaard voor de gebruiker in te stellen (optioneel, standaard ingesteld op de inhoudstaal).",
-       "apihelp-createaccount-example-pass": "Account <kbd>testuser</kbd> met wachtwoord <kbd>test123</kbd> aanmaken.",
-       "apihelp-createaccount-example-mail": "Account <kbd>testmailuser</kbd> aanmaken en een willekeurig gegenereerd wachtwoord e-mailen.",
        "apihelp-delete-summary": "Een pagina verwijderen.",
        "apihelp-delete-param-title": "Titel van de te verwijderen pagina. Kan niet in combinatie met <var>$1pageid</var> gebruikt worden.",
        "apihelp-delete-param-pageid": "Pagina-ID van de te verwijderen pagina. Kan niet in combinatie met <var>$1title</var> gebruikt worden.",
index 310708b..fe98013 100644 (file)
        "apihelp-compare-param-toid": "Numer drugiej strony do porównania.",
        "apihelp-compare-param-torev": "Druga wersja do porównania.",
        "apihelp-createaccount-summary": "Utwórz nowe konto.",
-       "apihelp-createaccount-param-name": "Nazwa użytkownika",
-       "apihelp-createaccount-param-password": "Hasło (ignorowane jeśli ustawiono <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Domena uwierzytelniania zewnętrznego (opcjonalnie).",
-       "apihelp-createaccount-param-token": "Token tworzenia konta uzyskany w pierwszym zapytaniu.",
-       "apihelp-createaccount-param-email": "Adres email użytkownika (opcjonalne).",
-       "apihelp-createaccount-param-realname": "Prawdziwe imię i nazwisko użytkownika (opcjonalne).",
-       "apihelp-createaccount-param-reason": "Opcjonalny powód tworzenia konta, który zostanie umieszczony w rejestrze.",
-       "apihelp-createaccount-example-pass": "Utwórz użytkownika <kbd>testuser</kbd> z hasłem <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Utwórz użytkownika <kbd>testmailuser</kbd> i wyślij losowo wygenerowane hasło na emaila.",
        "apihelp-delete-summary": "Usuń stronę.",
        "apihelp-delete-param-reason": "Powód usuwania. Jeśli pozostawisz to pole puste, zostanie użyty powód wygenerowany automatycznie.",
        "apihelp-delete-param-watch": "Dodaj stronę do obecnej listy obserwowanych.",
index d7ba28b..4b0f603 100644 (file)
        "apihelp-createaccount-summary": "Criar uma nova conta de usuário.",
        "apihelp-createaccount-param-preservestate": "Se <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> retornar true para <samp>hasprimarypreservedstate</samp>, pedidos marcados como <samp>hasprimarypreservedstate</samp> devem ser omitidos. Se retornou um valor não vazio para <samp>preservedusername</samp>, esse nome de usuário deve ser usado pelo parâmetro <var>username</var>.",
        "apihelp-createaccount-example-create": "Inicie o processo de criação do usuário <kbd>Example</kbd> com a senha <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Nome de usuário.",
-       "apihelp-createaccount-param-password": "Senha (ignorada se <var>$1mailpassword</var> está definida).",
-       "apihelp-createaccount-param-domain": "Domínio para autenticação externa (opcional).",
-       "apihelp-createaccount-param-token": "Token de criação de conta obtido no primeiro pedido.",
-       "apihelp-createaccount-param-email": "Endereço de e-mail para o usuário (opcional).",
-       "apihelp-createaccount-param-realname": "Nome real do usuário (opcional).",
-       "apihelp-createaccount-param-mailpassword": "Se configurado para qualquer valor, uma senha aleatória será enviada por e-mail ao usuário.",
-       "apihelp-createaccount-param-reason": "Razão opcional para criar a conta a ser colocada nos logs.",
-       "apihelp-createaccount-param-language": "Código de idioma para definir como padrão para o usuário (opcional, padrão para o idioma do conteúdo).",
-       "apihelp-createaccount-example-pass": "Criar usuário <kbd>testuser</kbd> com senha <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Criar usuário <kbd>testmailuser</kbd> e enviar um e-mail com uma senha gerada aleatoriamente.",
        "apihelp-cspreport-summary": "Usado por navegadores para denunciar violações da Política de Segurança de Conteúdo. Este módulo nunca deve ser usado, exceto quando usado automaticamente por um navegador web compatível com CSP.",
        "apihelp-cspreport-param-reportonly": "Marque como sendo um relatório de uma política de monitoramento, não uma política forçada",
        "apihelp-cspreport-param-source": "O que gerou o cabeçalho CSP que desencadeou este relatório",
index d14ac5a..fc38b40 100644 (file)
        "apihelp-createaccount-summary": "Criar uma conta de utilizador nova.",
        "apihelp-createaccount-param-preservestate": "Se <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> devolveu o valor verdadeiro para <samp>hasprimarypreservedstate</samp>, pedidos marcados como <samp>primary-required</samp> devem ser omitidos. Se devolveu um valor não vazio em <samp>preservedusername</samp>, esse nome de utilizador tem de ser usado no parâmetro <var>username</var>.",
        "apihelp-createaccount-example-create": "Iniciar o processo de criação do utilizador <kbd>Example</kbd> com a palavra-passe <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Nome de utilizador.",
-       "apihelp-createaccount-param-password": "Palavra-passe (ignorada se <var>$1mailpassword</var> está definida).",
-       "apihelp-createaccount-param-domain": "Domínio para autenticação externa (opcional).",
-       "apihelp-createaccount-param-token": "Chave de criação da conta, obtida no primeiro pedido.",
-       "apihelp-createaccount-param-email": "Endereço de correio eletrónico do utilizador (opcional).",
-       "apihelp-createaccount-param-realname": "Nome verdadeiro do utilizador (opcional).",
-       "apihelp-createaccount-param-mailpassword": "Se qualquer valor estiver definido, uma palavra-passe aleatória será enviada por correio eletrónico ao utilizador.",
-       "apihelp-createaccount-param-reason": "Motivo opcional de criação da conta, para ser colocado nos registos.",
-       "apihelp-createaccount-param-language": "Código da língua a definir como padrão para o utilizador (opcional, por omissão é a língua de conteúdo).",
-       "apihelp-createaccount-example-pass": "Criar o utilizador <kbd>testuser</kbd> com a palavra-passe <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Criar o utilizador <kbd>testmailuser</kbd> e enviar por correio eletrónico uma palavra-passe gerada aleatoriamente.",
        "apihelp-cspreport-summary": "Usado por '' browsers'' para reportar violações da norma \"Content Security Policy\". Este módulo nunca deve ser usado, exceto quando utilizado automaticamente por um ''browser'' compatível com a CSP.",
        "apihelp-cspreport-param-reportonly": "Marcar como sendo um relatório vindo de uma norma de monitorização e não de uma norma exigida.",
        "apihelp-cspreport-param-source": "Aquilo que gerou o cabeçalho CSP que desencadeou este relatório.",
index bdd0afb..9abcfe2 100644 (file)
        "apihelp-createaccount-summary": "{{doc-apihelp-summary|createaccount}}",
        "apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}",
        "apihelp-createaccount-example-create": "{{doc-apihelp-example|createaccount}}",
-       "apihelp-createaccount-param-name": "{{doc-apihelp-param|createaccount|name}}\n{{Identical|Username}}",
-       "apihelp-createaccount-param-password": "{{doc-apihelp-param|createaccount|password}}",
-       "apihelp-createaccount-param-domain": "{{doc-apihelp-param|createaccount|domain}}",
-       "apihelp-createaccount-param-token": "{{doc-apihelp-param|createaccount|token}}",
-       "apihelp-createaccount-param-email": "{{doc-apihelp-param|createaccount|email}}",
-       "apihelp-createaccount-param-realname": "{{doc-apihelp-param|createaccount|realname}}",
-       "apihelp-createaccount-param-mailpassword": "{{doc-apihelp-param|createaccount|mailpassword}}",
-       "apihelp-createaccount-param-reason": "{{doc-apihelp-param|createaccount|reason}}",
-       "apihelp-createaccount-param-language": "{{doc-apihelp-param|createaccount|language}}",
-       "apihelp-createaccount-example-pass": "{{doc-apihelp-example|createaccount}}",
-       "apihelp-createaccount-example-mail": "{{doc-apihelp-example|createaccount}}",
        "apihelp-cspreport-summary": "{{doc-apihelp-summary|cspreport}}",
        "apihelp-cspreport-param-reportonly": "{{doc-apihelp-param|cspreport|reportonly}}",
        "apihelp-cspreport-param-source": "{{doc-apihelp-param|cspreport|source}}",
index 0206463..1b30d00 100644 (file)
        "apihelp-createaccount-summary": "Создание новой учётной записи.",
        "apihelp-createaccount-param-preservestate": "Если запрос <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> возвращает true для <samp>hasprimarypreservedstate</samp>, то запросы, отмеченные как <samp>primary-required</samp>, должны быть пропущены. Если запрос возвращает непустое значение поля <samp>preservedusername</samp>, то это значение должно быть использовано в параметре <samp>username</var>.",
        "apihelp-createaccount-example-create": "Начать создание участника <kbd>Example</kbd> с паролем <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Имя участника.",
-       "apihelp-createaccount-param-password": "Пароль (будет проигнорирован, если задан параметр <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Домен для внешней аутентификации (необязательно).",
-       "apihelp-createaccount-param-token": "Токен создания учётной записи, полученный в первом запросе.",
-       "apihelp-createaccount-param-email": "Адрес электронной почты участника (необязательно).",
-       "apihelp-createaccount-param-realname": "Настоящее имя участника (необязательно).",
-       "apihelp-createaccount-param-mailpassword": "При установке любого значения, случайный пароль будет выслан участнику по электронной почте.",
-       "apihelp-createaccount-param-reason": "Причина создания учетной записи для записи в журнал (необязательно).",
-       "apihelp-createaccount-param-language": "Языковой код, который будет установлен в качестве основного языка участника (необязательно, по умолчанию используется основной язык вики).",
-       "apihelp-createaccount-example-pass": "Создать участника <kbd>testuser</kbd> с паролем <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Создать участника <kbd>testmailuser</kbd> и прислать на электронную почту случайно сгенерированный пароль.",
        "apihelp-cspreport-summary": "Используется браузерами, чтобы сообщать о нарушениях политики безопасности (CSP). Этот модуль никогда не должен использоваться, за исключением случаев автоматического использования совместимыми с CSP браузерами.",
        "apihelp-cspreport-param-reportonly": "Отметить как доклад от политики мониторинга, не от принудительной политики",
        "apihelp-cspreport-param-source": "Что сгенерировало заголовок CSP, вызвавший этот доклад",
index bf5d89e..df94ce2 100644 (file)
        "apihelp-compare-param-torev": "Andra version att jämföra.",
        "apihelp-compare-example-1": "Skapa en diff mellan version 1 och 2",
        "apihelp-createaccount-summary": "Skapa ett nytt användarkonto.",
-       "apihelp-createaccount-param-name": "Användarnamn.",
-       "apihelp-createaccount-param-password": "Lösenord (ignoreras om <var>$1mailpassword</var> angetts).",
-       "apihelp-createaccount-param-domain": "Domän för extern autentisering (frivillig).",
-       "apihelp-createaccount-param-token": "Nyckel för kontoskapande erhölls i första begäran.",
-       "apihelp-createaccount-param-email": "Användarens e-postadress (valfritt).",
-       "apihelp-createaccount-param-realname": "Användarens riktiga namn (valfritt).",
-       "apihelp-createaccount-param-mailpassword": "Om satt till ett värde, skickas ett slumpmässigt lösenord till användaren via e-post.",
-       "apihelp-createaccount-param-reason": "Valfri anledning för att skapa kontot för att läggas till i loggarna.",
-       "apihelp-createaccount-param-language": "Språkkod att använda som standard för användaren (valfri, standardvärdet är innehållsspråket).",
-       "apihelp-createaccount-example-pass": "Skapa användaren <kbd>testuser</kbd> med lösenordet <kbd>test123</kbd>",
-       "apihelp-createaccount-example-mail": "Skapa användaren <kbd>testmailuser</kbd> och skicka ett slumpgenererat lösenord via e-post",
        "apihelp-delete-summary": "Radera en sida.",
        "apihelp-delete-param-title": "Titel på sidan du vill radera. Kan inte användas tillsammans med <var>$1pageid</var>.",
        "apihelp-delete-param-pageid": "Sid-ID för sidan att radera. Kan inte användas tillsammans med <var>$1titel</var>.",
index 0b2e6db..7bad7c0 100644 (file)
        "apihelp-createaccount-summary": "Створити новий обліковий запис користувача.",
        "apihelp-createaccount-param-preservestate": "Якщо запит <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> повернув істину для <samp>hasprimarypreservedstate</samp>, запити позначені як <samp>primary-required</samp> повинні бути пропущені. Якщо він повернув не порожнє значення для <samp>preservedusername</samp>, це ім'я користувача повинно бути використано для параметру <var>username</var>.",
        "apihelp-createaccount-example-create": "Почати процес створення користувача <kbd>Example</kbd> з паролем <kbd>ExamplePassword</kbd>.",
-       "apihelp-createaccount-param-name": "Ім'я користувача.",
-       "apihelp-createaccount-param-password": "Пароль (ігнорується, якщо встановлено <var>$1mailpassword</var>).",
-       "apihelp-createaccount-param-domain": "Домен для зовнішньої аутентифікації (опціонально).",
-       "apihelp-createaccount-param-token": "Токен створення облікового запису отримано у першому запиті.",
-       "apihelp-createaccount-param-email": "Адреса електронної пошти користувача (необов'язково).",
-       "apihelp-createaccount-param-realname": "Справжнє ім'я користувача (необов'язково).",
-       "apihelp-createaccount-param-mailpassword": "Якщо встановлено будь-яке значення, користувачеві буде надіслано випадковий пароль.",
-       "apihelp-createaccount-param-reason": "Необов'язкова причина для створення облікового запису, яка буде записана в журнал.",
-       "apihelp-createaccount-param-language": "Код мови для встановлення за замовчуванням для користувача (необов'язково, за замовчуванням — мова вмісту).",
-       "apihelp-createaccount-example-pass": "Створити користувача <kbd>testuser</kbd> з паролем <kbd>test123</kbd>.",
-       "apihelp-createaccount-example-mail": "Створити користувача <kbd>testmailuser</kbd> і надіслати на електронну пошту випадково-згенерований пароль.",
        "apihelp-cspreport-summary": "Використовується браузерами для повідомлення порушень Правил безпеки контенту (Content Security Policy). Цей модуль не повинен використовуватися, окрім випадків автоматичного використання веб-браузером для CSP-скарги.",
        "apihelp-cspreport-param-reportonly": "Позначити як доповідь із моніторингової політики, не примусової політики",
        "apihelp-cspreport-param-source": "Що згенерувало CSP-заголовок, який запустив цю доповідь",
index cee1b7a..78f92f1 100644 (file)
        "apihelp-createaccount-summary": "创建一个新用户账户。",
        "apihelp-createaccount-param-preservestate": "如果<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>返回用于<samp>hasprimarypreservedstate</samp>的真值,标记为<samp>primary-required</samp>的请求应被忽略。如果它返回用于<samp>preservedusername</samp>的非空值,用户名必须用于<var>username</var>参数。",
        "apihelp-createaccount-example-create": "开始创建用户<kbd>Example</kbd>和密码<kbd>ExamplePassword</kbd>的过程。",
-       "apihelp-createaccount-param-name": "用户名。",
-       "apihelp-createaccount-param-password": "密码(如果设置<var>$1mailpassword</var>则忽略)。",
-       "apihelp-createaccount-param-domain": "外部身份验证域 (可选)。",
-       "apihelp-createaccount-param-token": "在第一个请求中获得的帐户创建标记。",
-       "apihelp-createaccount-param-email": "用户的电子邮件地址(可选)。",
-       "apihelp-createaccount-param-realname": "用户的真实姓名(可选)。",
-       "apihelp-createaccount-param-mailpassword": "如果设置为任何值,将向用户发送一个随机密码。",
-       "apihelp-createaccount-param-reason": "将要放在日志中的,关于创建帐户的可选原因。",
-       "apihelp-createaccount-param-language": "要为用户设置为默认值的语言代码(可选,默认为内容语言)。",
-       "apihelp-createaccount-example-pass": "创建用户<kbd>testuser</kbd>和密码<kbd>test123</kbd>。",
-       "apihelp-createaccount-example-mail": "创建用户<kbd>testmailuser</kbd>并电邮发送一个随机生成的密码。",
        "apihelp-cspreport-summary": "由浏览器使用以报告违反内容安全方针的内容。此模块应永不使用,除了在被CSP兼容的浏览器自动使用时。",
        "apihelp-cspreport-param-reportonly": "标记作为来自监视方针的报告,而不是执行方针的报告",
        "apihelp-cspreport-param-source": "生成引发此报告的CSP标头的事物",
index 91129de..776c59a 100644 (file)
@@ -23,6 +23,7 @@
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|說明文件]]\n* [[mw:Special:MyLanguage/API:FAQ|常見問題]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api 郵寄清單]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API公告]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R 報告錯誤及請求功能]\n</div>\n<strong>狀態資訊:</strong>MediaWiki API 已是成熟、穩定,並積極支援以改善的介面。儘管我們儘可能避免,但仍偶有需要重大變更的情況,請訂閱[https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce 郵寄清單]以便獲得更新通知。\n\n<strong>錯誤的請求:</strong>當 API 收到錯誤的請求,會發出以「MediaWiki-API-Error」為鍵的 HTTP 標頭欄位,隨後標頭欄位的值,以及傳回的錯誤碼會設為相同值。詳細資訊請參閱 [[mw:Special:MyLanguage/API:Errors_and_warnings|API: 錯誤與警告]]。\n\n<p class=\"mw-apisandbox-link\"><strong>測試:</strong>要簡化 API 請求的測試過程,請見 [[Special:ApiSandbox]]。</p>",
        "apihelp-main-param-action": "要執行的動作。",
        "apihelp-main-param-format": "輸出的格式。",
+       "apihelp-main-param-maxlag": "最大延遲可在當 MediaWiki 安裝於資料庫複寫叢集時使用。為了保存引起更多站台複寫延遲的操作,此參數可讓客戶端等待至複寫延遲小於指定值為止。在過渡延遲的情況下,錯誤碼 <samp>maxlag</samp> 會帶有著像是 <samp>Waiting for $host: $lag seconds lagged</samp> 的訊息內容回傳。<br />請查看[[mw:Special:MyLanguage/Manual:Maxlag_parameter|手冊:Maxlag 參數]]來獲取更多資訊。",
        "apihelp-main-param-smaxage": "將HTTP緩存控制頭欄位設為<code>s-maxage</code>秒。錯誤不會做緩存。",
        "apihelp-main-param-maxage": "將HTTP緩存控制頭欄位設為<code>max-age</code>秒。錯誤不會做緩存。",
        "apihelp-main-param-assert": "若設為<kbd>user</kbd>,會確認使用者是否已登入;若設為<kbd>bot</kbd>,會確認是否擁有機械人權限。",
@@ -31,6 +32,7 @@
        "apihelp-main-param-servedby": "在結果中包括提出請求的主機名。",
        "apihelp-main-param-curtimestamp": "在結果中包括目前的時間戳記。",
        "apihelp-main-param-responselanginfo": "在結果中包括<var>uselang</var>和<var>errorlang</var>所用的語言。",
+       "apihelp-main-param-origin": "當使用跨網域 AJAX 請求(cross-domain AJAX request、CORS)來存取 API 時,設定此為起始網域。這必須包含在任何預檢請求裡,因此得是請求 URI 的一部份(不是 POST 主體)。\n\n對於已認證請求,這必須準確地符合在 <code>Origin</code> 標頭裡其一的起始點,因此會被設定成像是 <kbd>https://zh.wikipedia.org</kbd> 或是 <kbd>https://meta.wikimedia.org</kbd>。如果此參數不符合 <code>Origin</code> 標頭,會回傳 403 錯誤回應。若此參數符合 <code>Origin</code> 標頭且起始點被列在白名單裡,將會設定 <code>Access-Control-Allow-Origin</code> 與 <code>Access-Control-Allow-Credentials</code> 標頭。\n\n對於非認證請求,會指定值 <kbd>*</kbd>。這會產生 <code>Access-Control-Allow-Origin</code> 標頭有被設定;但 <code>Access-Control-Allow-Credentials</code> 會是 <code>false</code> 值,且所有使用者指定資料會受限制。",
        "apihelp-main-param-uselang": "訊息翻譯採用的語言。使用 <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> 與 <kbd>siprop=languages</kbd> 會回傳語言代碼清單、或指定 <kbd>user</kbd> 來使用目前使用者的語言偏好設定、或是指定 <kbd>content</kbd> 來使用此 wiki 的內容語言。",
        "apihelp-main-param-errorformat": "用於警告和錯誤文字輸出的格式。\n; plaintext:移除掉 HTML 標籤,且實體已替換的 wiki 文字。\n; wikitext:未解析的 wiki 文字。\n; html:HTML。\n; raw:訊息鍵值與參數。\n; none:無文字輸出,僅含有錯誤代碼。\n; bc:用於 MediaWiki 1.29 之前版本的格式。會忽略 <var>errorlang</var> 與 <var>errorsuselocal</var>。",
        "apihelp-main-param-errorlang": "警告與錯誤採用的語言。使用 <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> 與 <kbd>siprop=languages</kbd> 會回傳語言代碼清單、或指定 <kbd>content</kbd> 來使用此 wiki 的內容語言、或是指定 <kbd>uselang</kbd> 來使用與 <var>uselang</var> 參數相同的值。",
@@ -57,7 +59,7 @@
        "apihelp-changeauthenticationdata-summary": "為目前使用者變更身分核對資料。",
        "apihelp-changeauthenticationdata-example-password": "嘗試更改目前使用者的密碼至 <kbd>ExamplePassword</kbd>。",
        "apihelp-checktoken-summary": "檢查來自 <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> 的權杖有效性。",
-       "apihelp-checktoken-param-type": "要測試的密鑰類型。",
+       "apihelp-checktoken-param-type": "要測試的權杖類型。",
        "apihelp-checktoken-param-token": "要測試的權杖。",
        "apihelp-checktoken-param-maxtokenage": "權杖的有效期間,以秒為單位。",
        "apihelp-checktoken-example-simple": "測試 <kbd>csrf</kbd> 權杖的有效性。",
        "apihelp-createaccount-summary": "建立新使用者帳號。",
        "apihelp-createaccount-param-preservestate": "如果 <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> 回傳 <samp>hasprimarypreservedstate</samp> 的 true 值,標記成 <samp>primary-required</samp> 的請求會被忽略。若回傳用於 <samp>preservedusername</samp> 的非零值,使用者名稱必須用於 <var>username</var> 參數。",
        "apihelp-createaccount-example-create": "開始建立使用者 <kbd>Example</kbd> 與密碼 <kbd>ExamplePassword</kbd> 的過程。",
-       "apihelp-createaccount-param-name": "使用者名稱。",
-       "apihelp-createaccount-param-password": "密碼 (若有設定 <var>$1mailpassword</var> 則可略過)。",
-       "apihelp-createaccount-param-domain": "外部身分核對使用的網域 (可有可無)。",
-       "apihelp-createaccount-param-token": "在第一次請求時已取得的帳號建立權杖。",
-       "apihelp-createaccount-param-email": "使用者的電子郵件地址 (可有可無) 。",
-       "apihelp-createaccount-param-realname": "使用者的真實姓名 (可有可無)。",
-       "apihelp-createaccount-param-mailpassword": "若設為其他值,將會以電子郵件寄送隨機密碼給使用者。",
-       "apihelp-createaccount-param-reason": "建立帳號時選填的原因,會被記錄到日誌當中。",
-       "apihelp-createaccount-param-language": "要設定的使用者預設語言代碼 (選填,預設依據內容語言)。",
-       "apihelp-createaccount-example-pass": "建立使用者 <kbd>testuser</kbd> 使用密碼 <kbd>test123</kbd>",
-       "apihelp-createaccount-example-mail": "建立使用者 <kbd>testmailuser</kbd> 並且電子郵件通知隨機產生的密碼。",
        "apihelp-cspreport-summary": "由瀏覽器所使用來回報違反內容安全方針。此模組應永不使用,除了是在被由兼容內容安全方針的網路瀏覽器所使用情況下。",
        "apihelp-cspreport-param-reportonly": "標記為來自監視方針而非強制方針的回報",
        "apihelp-cspreport-param-source": "生成觸發此報告之 CSP 標頭的事物",
        "apihelp-logout-summary": "登出並清除 session 資料。",
        "apihelp-logout-example-logout": "登出當前使用者",
        "apihelp-managetags-summary": "執行相關到更改標籤的管理任務。",
+       "apihelp-managetags-param-operation": "要執行的操作:\n;create:創建一個供手動使用的新更改標籤。\n;delete:從資料庫移除一個更改標籤,包含移除來自於所有修訂、近期變更項目以及日誌項目的所被使用標籤。\n;activate:啟用一個更改標籤,允許使用者手動套用。\n;deactivate:棄用一個更改標籤,不允許使用者手動套用。",
        "apihelp-managetags-param-tag": "要創建、刪除、啟用或停用的標籤。要標籤創建,標籤必須不存在。要標籤刪除,標籤必須存在。要標籤啟用,標籤必須存在且不能被任何擴充使用到。要標籤停用,標籤必須目前為啟用並且有被手動定義。",
        "apihelp-managetags-param-reason": "創建、刪除、啟用、或停用標籤的原因(可選)。",
        "apihelp-managetags-param-ignorewarnings": "是否在處理期間發生問題時忽略任何警告。",
        "apihelp-paraminfo-example-1": "顯示 <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>、<kbd>[[Special:ApiHelp/jsonfm|format=jsonfm]]</kbd>、<kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>、和 <kbd>[[Special:ApiHelp/query+siteinfo|action=query&meta=siteinfo]]</kbd> 的資訊。",
        "apihelp-paraminfo-example-2": "顯示 <kbd>[[Special:ApiHelp/query|action=query]]</kbd> 所有子模組的資訊。",
        "apihelp-parse-summary": "解析內容併回傳解析器輸出。",
+       "apihelp-parse-extended-description": "查看 <kbd>[[Special:ApiHelp/query|action=query]]</kbd> 的各種道具模組來從目前頁面版本取得資訊。\n\n以下有幾種方式來解析指定文字:\n# 要指定頁面或修訂,請使用 <var>$1page</var>、<var>$1pageid</var>、或是 <var>$1oldid</var>。\n# 要指定明確內容,請使用 <var>$1text</var>、<var>$1title</var>、<var>$1revid</var> 以及 <var>$1contentmodel</var>。\n# 要指定解析的摘要,<var>$1prop</var> 應提供一個空值。",
        "apihelp-parse-param-title": "文字所屬的頁面標題。若省略,需要指定 <var>$1contentmodel</var>,且 [[API]] 會用為標題使用。",
        "apihelp-parse-param-text": "要解析的文字。使用 <var>$1title</var> 或 <var>$1contentmodel</var> 來控制內容模組。",
        "apihelp-parse-param-revid": "修訂 ID,用於 <code><nowiki>{{REVISIONID}}</nowiki></code> 和相似變數。",
        "apihelp-query+blocks-paramvalue-prop-expiry": "添加當封鎖到期的時間戳記。",
        "apihelp-query+blocks-paramvalue-prop-reason": "添加封鎖的原因。",
        "apihelp-query+blocks-paramvalue-prop-range": "添加受封鎖影響的 IP 地址範圍。",
+       "apihelp-query+blocks-paramvalue-prop-flags": "以自動封鎖、僅限匿名、或其它來標記該禁令。",
        "apihelp-query+blocks-paramvalue-prop-restrictions": "若封鎖非整個網站範圍,添加部份封鎖限制。",
        "apihelp-query+blocks-param-show": "僅顯示符合這些標準的項目。\n例如僅想查看在 IP 地址的無限期封鎖,請設定 <kbd>$1show=ip|!temp</kbd>。",
        "apihelp-query+blocks-example-simple": "列出封鎖。",
        "apihelp-query+deletedrevisions-example-titles": "列出頁面 <kbd>Main Page</kbd> 與 <kbd>Talk:Main Page</kbd> 包含到內容的已刪除修訂。",
        "apihelp-query+deletedrevisions-example-revids": "列出已刪除修訂 <kbd>123456</kbd> 的資訊。",
        "apihelp-query+deletedrevs-summary": "列出已刪除的修訂。",
+       "apihelp-query+deletedrevs-extended-description": "以三種模式操作:\n# 列出指定標題的已刪除修訂,按時間戳記排序。\n# 列出指定使用者的已刪除貢獻,按時間戳記排序(無指定標題)。\n# 列出在指定命名空間內的所有已刪除修訂,按標題與時間戳記排序(無指定標題、未設定 $1user)。\n\n一些參數僅套用於某些模式,並且其它模式裡會被忽略。",
        "apihelp-query+deletedrevs-paraminfo-modes": "{{PLURAL:$1|模式|模式}}:$2",
        "apihelp-query+deletedrevs-param-start": "起始列舉的時間戳記。",
        "apihelp-query+deletedrevs-param-end": "終止列舉的時間戳記。",
        "apihelp-query+deletedrevs-param-excludeuser": "不要列出由該使用者作出的修訂。",
        "apihelp-query+deletedrevs-param-namespace": "僅列出此命名空間的頁面。",
        "apihelp-query+deletedrevs-param-limit": "修訂能列出的最大數量。",
+       "apihelp-query+deletedrevs-param-prop": "要取得的參數:\n;revidv:添加已刪除修訂的修訂 ID。\n;parentid:添加前一個修訂的修訂 ID 至頁面。\n;user:添加做出修訂的使用者。\n;userid:添加做出修訂的使用者 ID。\n;comment:添加修訂的註釋。\n;parsedcomment:添加修訂的解析註釋。\n;minor:標記修訂是否為小編輯。\n;len:添加修訂的長度(位元組)。\n;sha1:添加修訂的 SHA-1 雜湊(base 16)。\n;content:添加修訂內容。\n;token:<span class=\"apihelp-deprecated\">已棄用。</span>提供編輯權杖。\n;tags:修訂的標籤。",
        "apihelp-query+deletedrevs-example-mode1": "以帶有內容(模式 1)列出頁面 <kbd>Main Page</kbd> 與 <kbd>Talk:Main Page</kbd> 的最新刪除修訂。",
        "apihelp-query+deletedrevs-example-mode2": "列出最近前 50 個已刪除掉由 <kbd>Bob</kbd> 所做出的貢獻(模式 2)。",
        "apihelp-query+deletedrevs-example-mode3-main": "列出在主命名空間的前 50 個已刪除修訂(模式 3)。",
        "apihelp-query+iwlinks-param-dir": "列出時所採用的方向。",
        "apihelp-query+iwlinks-example-simple": "從頁面 <kbd>Main Page</kbd> 取得跨 wiki 連結。",
        "apihelp-query+langbacklinks-summary": "找出連結至指定語言連結的所有頁面。",
+       "apihelp-query+langbacklinks-extended-description": "可用來查找帶有語言代碼的所有連結、或是所有標題連結(帶指定語言)。不使用任何參數代表著「所有語言連結」。\n\n請注意這可能不會考慮由擴充功能所添加的語言連結。",
        "apihelp-query+langbacklinks-param-lang": "用於語言的語言連結。",
        "apihelp-query+langbacklinks-param-title": "要搜尋的語言連結。必須與$1lang一同使用。",
        "apihelp-query+langbacklinks-param-limit": "要回傳的頁面總數。",
        "apihelp-query+pageswithprop-example-simple": "列出前 10 個使用 <code>&#123;&#123;DISPLAYTITLE:&#125;&#125;</code> 的頁面。",
        "apihelp-query+pageswithprop-example-generator": "取得前 10 個使用到 <code>_&#95;NOTOC_&#95;</code> 頁面的額外資訊。",
        "apihelp-query+prefixsearch-summary": "執行頁面標題的前綴搜尋。",
+       "apihelp-query+prefixsearch-extended-description": "儘管名稱上相似,此模組不代表相同於 [[Special:PrefixIndex]]。對於此請以 <kbd>apprefix</kbd> 參數來查看 <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd>。此模組的目的相似於 <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>:獲取使用者的輸入內容,並提供最相符的標題。依搜尋引擎後端的情況,可能會有包含錯別字修正、避免重新導向、或是其它探索的行為。",
        "apihelp-query+prefixsearch-param-search": "搜尋字串。",
        "apihelp-query+prefixsearch-param-namespace": "搜尋的命名空間。若 <var>$1search</var> 以有效的命名空間前綴為開頭則會被忽略。",
        "apihelp-query+prefixsearch-param-limit": "回傳的結果數量上限。",
        "apihelp-query+revisions+base-paramvalue-prop-tags": "修訂標籤。",
        "apihelp-query+revisions+base-paramvalue-prop-roles": "列出存在於修訂的內容間隔作用。",
        "apihelp-query+revisions+base-paramvalue-prop-parsetree": "請改用 <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> 或 <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>。修訂內容的 XML 解析樹狀(需要內容模組 <code>$1</code>)。",
+       "apihelp-query+revisions+base-param-slots": "當間隔相關屬性包含在 <var>$1props</var> 時,要回傳資料的修訂間隔。若忽略的話,來自 <kbd>main</kbd> 間隔的資料會以向下兼容格式回傳。",
        "apihelp-query+revisions+base-param-limit": "限制所回傳的修訂數量。",
        "apihelp-query+revisions+base-param-expandtemplates": "請改用 <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd>。在修訂內容裡展開模板(需要 $1prop=content)。",
        "apihelp-query+revisions+base-param-generatexml": "請改用 <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> 或 <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd>。產生用於修訂內容的 XML 解析樹狀(需要 $1prop=content)。",
        "apihelp-query+usercontribs-paramvalue-prop-comment": "添加編輯的註釋。",
        "apihelp-query+usercontribs-paramvalue-prop-parsedcomment": "添加編輯的解析註釋。",
        "apihelp-query+usercontribs-paramvalue-prop-size": "添加編輯的新大小。",
+       "apihelp-query+usercontribs-paramvalue-prop-sizediff": "添加對於其父級編輯的編輯大小增量。",
        "apihelp-query+usercontribs-paramvalue-prop-flags": "添加編輯的標籤。",
        "apihelp-query+usercontribs-paramvalue-prop-patrolled": "標記已巡查編輯。",
        "apihelp-query+usercontribs-paramvalue-prop-autopatrolled": "標記自動巡查編輯。",
        "apihelp-setpagelanguage-example-language": "更改 <kbd>Main Page</kbd> 的語言成巴斯克語。",
        "apihelp-setpagelanguage-example-default": "將 ID 是 123 頁面的語言更改為 wiki 的預設內容語言。",
        "apihelp-stashedit-summary": "在分享快取裡預備編輯。",
+       "apihelp-stashedit-extended-description": "此意指使用透過自編輯表單的 AJAX 來改善頁面儲存的效能。",
        "apihelp-stashedit-param-title": "正在編輯此頁面的標題。",
        "apihelp-stashedit-param-section": "章節編號。<kbd>0</kbd> 代表最上層章節,<kbd>new</kbd> 代表新章節。",
        "apihelp-stashedit-param-sectiontitle": "新章節的標題。",
        "apihelp-unlinkaccount-summary": "移除目前使用者所連結到的第三方帳號。",
        "apihelp-unlinkaccount-example-simple": "以 <kbd>FooAuthenticationRequest</kbd> 嘗試移除目前使用者的關聯供應者連結.",
        "apihelp-upload-summary": "上傳檔案,或取得等待上傳的狀態。",
+       "apihelp-upload-extended-description": "有以下方式可用:\n* 直接上傳檔案內容,請使用 <var>$1file</var> 參數。\n* 分批上傳檔案,請使用 <var>$1filesize</var>、<var>$1chunk</var>、以及 <var>$1offset</var> 參數。\n* 擁有 MediaWiki 伺服器來從 URL 來檢索檔案,請使用 <var>$1url</var> 參數。\n* 要完成一個稍早出於警告而失敗的上傳,請使用 <var>$1filekey</var> 參數。\n請注意當發送 <var>$1file</var> 時,HTTP POST 必須用做為檔案上傳(註:使用 <code>multipart/form-data</code>)。",
        "apihelp-upload-param-filename": "目標檔案名稱。",
        "apihelp-upload-param-comment": "上傳註釋。如果 <var>$1text</var> 未指定的話,也會作為新檔案用的初始頁面文字。",
        "apihelp-upload-param-tags": "更改標籤來套用到上傳日誌項目以及檔案頁面修訂。",
        "apihelp-userrights-param-user": "使用者名稱。",
        "apihelp-userrights-param-userid": "使用者ID。",
        "apihelp-userrights-param-add": "加入使用者至這些群組;若已是成員,則更新期限時間。",
+       "apihelp-userrights-param-expiry": "期限時間戳記。可以是相對時間(例如:<kbd>5 months</kbd> 或 <kbd>2 weeks</kbd>)或是絕對時間(例如:<kbd>2014-09-18T12:34:56Z</kbd>)。如果僅設定一個時間戳記,會為所有群組傳遞給 <var>$1add</var> 參數。對於永不逾期的群組,請使用 <kbd>infinite</kbd>、<kbd>infinity</kbd>、或 <kbd>never</kbd>。",
        "apihelp-userrights-param-remove": "從這些群組移除使用者。",
        "apihelp-userrights-param-reason": "變更的原因。",
        "apihelp-userrights-param-tags": "在使用者權限日誌裡更改套用到項目的標籤。",
        "api-help-datatypes-header": "資料類型",
        "api-help-datatypes": "至MediaWiki的輸入值應為NFC標準化的UTF-8。MediaWiki可以嘗試轉換其他輸入值,但這可能導致一些操作失敗(例如附帶MD5檢查的[[Special:ApiHelp/edit|編輯]])。\n\n一些在API請求中的參數類型需要更進一步解釋:\n;boolean\n:布林參數產生作用就像HTML複選框一樣:如果參數被指定,無論何值都被視為真(true)。如果要假值(false),則必須省略參數。\n;timestamp\n:時間戳記可被指定為多種格式。推荐使用ISO 8601日期和時間標準。所有時間為UTC時間,包含的任何時區都會被忽略。\n:* ISO 8601日期和時間,<kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>(標點和<kbd>Z</kbd>為選用)\n:* 帶小數秒(會被忽略)的ISO 8601日期和時間,<kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd>(破折號、冒號和<kbd>Z</kbd>為選用)\n:* MediaWiki格式,<kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* 一般數字格式,<kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>(<kbd>GMT</kbd>、<kbd>+<var>##</var></kbd>或<kbd>-<var>##</var></kbd>的選用時區會被忽略)\n:* EXIF格式,<kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 2822格式(時區可省略),<kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850格式(時區可省略),<kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime格式,<kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* 從1970-01-01T00:00:00Z開始的秒數,作為1到13位數的整數(除了<kbd>0</kbd>)\n:* 字串<kbd>now</kbd>\n;替代多值分隔符號\n:使用多個值的參數通常會與垂直線符號(|)分隔的值一起提交,例如<kbd>param=value1|value2</kbd>或<kbd>param=value1%7Cvalue2</kbd>。如果值必須包含垂直線符號,使用U+001F(單位分隔符號)作為分隔符號,''並且''在值前加前綴U+001F,例如<kbd>param=%1Fvalue1%1Fvalue2</kbd>。",
        "api-help-templatedparams-header": "模板參數",
+       "api-help-templatedparams": "模板參數可支援當 API 模組需要替某些參數值給予值的情況。舉例來說,如果有個用來請求水果的 API 模組,可能會有一個用來指定水果的 <var>fruits</var> 參數,以及用來指定有多少顆水果的模板參數 <var>{fruit}-quantity</var>。若一個 API 客戶端想要 1 顆蘋果、5 條香蕉、以及 20 粒草莓時,可以做出像是 <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd> 這樣的請求。",
        "api-help-param-type-limit": "類型:整數或<kbd>max</kbd>",
        "api-help-param-type-integer": "類型:{{PLURAL:$1|1=整數|2=整數列表}}",
        "api-help-param-type-boolean": "類型:布林值([[Special:ApiHelp/main#main/datatypes|詳細資訊]])",
        "apierror-cantsend": "您尚未登入,您沒有已確認的電子郵件地址,或是您未被允許發送電子郵件給其他人,因此您不能發送電子郵件。",
        "apierror-cantundelete": "無法取消刪除:請求的修訂可能不存在,或是可能已被取消刪除。",
        "apierror-changeauth-norequest": "建立更改請求失敗。",
+       "apierror-chunk-too-small": "對於非最終塊,最小塊的大小為 $1 {{PLURAL:$1|位元組|位元組}}。",
        "apierror-cidrtoobroad": "不能接受超出 /$2 的 $1 CIDR 範圍。",
        "apierror-compare-maintextrequired": "當 <var>$1slots</var> 包含 <kbd>main</kbd> 時,需要參數 <var>$1text-main</var>(不能刪除主要部份)。",
        "apierror-compare-no-title": "無法在不帶標題之下預先儲存轉換。請嘗試指定 <var>fromtitle</var> 或 <var>totitle</var>。",
        "apierror-compare-notext": "參數 <var>$1</var> 不能在缺少 <var>$2</var> 的情況下使用。",
        "apierror-compare-notorevision": "沒有「to」修訂。請指定 <var>torev</var>、<var>totitle</var>、或 <var>toid</var>。",
        "apierror-compare-relative-to-deleted": "相關已刪除修訂時不能使用 <kbd>torelative=$1</kbd>。",
+       "apierror-compare-relative-to-nothing": "沒有相關 <var>torelative</var> 的 'from' 修訂。",
        "apierror-contentserializationexception": "內容序列化失敗:$1",
        "apierror-contenttoobig": "您所提供的內容超出條目的 $1 {{PLURAL:$1|位元組|位元組}}限制。",
        "apierror-copyuploadbaddomain": "不允許從此網域來透過 URL 上傳。",
        "apierror-integeroutofrange-abovemax": "對於使用者而言,<var>$1</var> 不能超過 $2(設定為 $3)。",
        "apierror-integeroutofrange-belowminimum": "<var>$1</var> 不能小於 $2(設定為 $3)。",
        "apierror-invalidcategory": "您所輸入的分類名稱無效。",
+       "apierror-invalid-chunk": "偏移量加上目前部分大於所聲稱的檔案大小。",
        "apierror-invalidexpiry": "無效的期限時間「$1」。",
        "apierror-invalid-file-key": "不是有效的檔案鍵。",
        "apierror-invalidlang": "用於參數 <var>$1</var> 的語言代碼無效。",
        "apierror-mimesearchdisabled": "MIME 搜尋在 Miser 模式裡被停用。",
        "apierror-missingcontent-pageid": "遺失頁面 ID 為 $1 的內容。",
        "apierror-missingcontent-revid": "遺失修訂 ID 為 $1 的內容。",
+       "apierror-missingcontent-revid-role": "作用$2的修訂 ID $1 缺少內容。",
        "apierror-missingparam-at-least-one-of": "參數$1{{PLURAL:$2||其一}}為必要。",
        "apierror-missingparam-one-of": "參數$1{{PLURAL:$2||其一}}為必要。",
        "apierror-missingparam": "<var>$1</var>參數必須被設定。",
        "apierror-stashedfilenotfound": "在儲藏裡找不到檔案:$1。",
        "apierror-stashedit-missingtext": "給予的雜湊裡查無儲藏文字。",
        "apierror-stashfailed-complete": "大量上傳已完成,請檢查狀態來獲取詳情。",
+       "apierror-stashfailed-nosession": "沒有帶此鍵的分塊上傳 session。",
        "apierror-stashfilestorage": "在儲藏裡不能儲存上傳:$1。",
        "apierror-stashinvalidfile": "無效的儲藏檔案。",
        "apierror-stashnosuchfilekey": "沒有這樣的檔案鍵:$1。",
        "apiwarn-unclearnowtimestamp": "傳遞給時間戳記參數 <var>$1</var> 的值「$2」已被棄用。若出於某些原因您需要不計算客戶端來明確指定時間,請使用 <kbd>now</kbd>。",
        "apiwarn-unrecognizedvalues": "參數 <var>$1</var> 有無法識別的{{PLURAL:$3|值|值}}:$2。",
        "apiwarn-unsupportedarray": "參數 <var>$1</var> 使用了不被支援的 PHP 陣列語法。",
+       "apiwarn-urlparamwidth": "忽略設定在 <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> are allowed)。",
        "apiwarn-validationfailed-badpref": "不是有效的偏好設定。",
        "apiwarn-validationfailed-cannotset": "不能透過此模組設定。",
index 93685e3..d7669eb 100644 (file)
  * @author Jeroen De Dauw < jeroendedauw@gmail.com >
  */
 
-/**
- * Interface for all classes implementing CacheHelper functionality.
- *
- * @since 1.20
- */
-interface ICacheHelper {
-       /**
-        * Sets if the cache should be enabled or not.
-        *
-        * @since 1.20
-        * @param bool $cacheEnabled
-        */
-       function setCacheEnabled( $cacheEnabled );
-
-       /**
-        * Initializes the caching.
-        * Should be called before the first time anything is added via addCachedHTML.
-        *
-        * @since 1.20
-        *
-        * @param int|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp.
-        * @param bool|null $cacheEnabled Sets if the cache should be enabled or not.
-        */
-       function startCache( $cacheExpiry = null, $cacheEnabled = null );
-
-       /**
-        * Get a cached value if available or compute it if not and then cache it if possible.
-        * The provided $computeFunction is only called when the computation needs to happen
-        * and should return a result value. $args are arguments that will be passed to the
-        * compute function when called.
-        *
-        * @since 1.20
-        *
-        * @param callable $computeFunction
-        * @param array|mixed $args
-        * @param string|null $key
-        *
-        * @return mixed
-        */
-       function getCachedValue( $computeFunction, $args = [], $key = null );
-
-       /**
-        * Saves the HTML to the cache in case it got recomputed.
-        * Should be called after the last time anything is added via addCachedHTML.
-        *
-        * @since 1.20
-        */
-       function saveCache();
-
-       /**
-        * Sets the time to live for the cache, in seconds or a unix timestamp
-        * indicating the point of expiry...
-        *
-        * @since 1.20
-        *
-        * @param int $cacheExpiry
-        */
-       function setExpiry( $cacheExpiry );
-}
-
 use MediaWiki\MediaWikiServices;
 
 /**
diff --git a/includes/cache/ICacheHelper.php b/includes/cache/ICacheHelper.php
new file mode 100644 (file)
index 0000000..54e9aac
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Cache of various elements in a single cache entry.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @license GPL-2.0-or-later
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+
+/**
+ * Interface for all classes implementing CacheHelper functionality.
+ *
+ * @since 1.20
+ */
+interface ICacheHelper {
+       /**
+        * Sets if the cache should be enabled or not.
+        *
+        * @since 1.20
+        * @param bool $cacheEnabled
+        */
+       function setCacheEnabled( $cacheEnabled );
+
+       /**
+        * Initializes the caching.
+        * Should be called before the first time anything is added via addCachedHTML.
+        *
+        * @since 1.20
+        *
+        * @param int|null $cacheExpiry Sets the cache expiry, either ttl in seconds or unix timestamp.
+        * @param bool|null $cacheEnabled Sets if the cache should be enabled or not.
+        */
+       function startCache( $cacheExpiry = null, $cacheEnabled = null );
+
+       /**
+        * Get a cached value if available or compute it if not and then cache it if possible.
+        * The provided $computeFunction is only called when the computation needs to happen
+        * and should return a result value. $args are arguments that will be passed to the
+        * compute function when called.
+        *
+        * @since 1.20
+        *
+        * @param callable $computeFunction
+        * @param array|mixed $args
+        * @param string|null $key
+        *
+        * @return mixed
+        */
+       function getCachedValue( $computeFunction, $args = [], $key = null );
+
+       /**
+        * Saves the HTML to the cache in case it got recomputed.
+        * Should be called after the last time anything is added via addCachedHTML.
+        *
+        * @since 1.20
+        */
+       function saveCache();
+
+       /**
+        * Sets the time to live for the cache, in seconds or a unix timestamp
+        * indicating the point of expiry...
+        *
+        * @since 1.20
+        *
+        * @param int $cacheExpiry
+        */
+       function setExpiry( $cacheExpiry );
+}
index b9944a8..b3dc004 100644 (file)
@@ -189,22 +189,6 @@ class LinkCache {
                $this->goodLinks->clear( $dbkey );
        }
 
-       /**
-        * Add a title to the link cache, return the page_id or zero if non-existent
-        *
-        * @deprecated since 1.27, unused
-        * @param string $title Prefixed DB key
-        * @return int Page ID or zero
-        */
-       public function addLink( $title ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               $nt = Title::newFromDBkey( $title );
-               if ( !$nt ) {
-                       return 0;
-               }
-               return $this->addLinkObj( $nt );
-       }
-
        /**
         * Fields that LinkCache needs to select
         *
index 9c20862..21b262a 100644 (file)
@@ -106,7 +106,7 @@ class LocalisationCache {
        /**
         * All item keys
         */
-       static public $allKeys = [
+       public static $allKeys = [
                'fallback', 'namespaceNames', 'bookstoreList',
                'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
                'separatorTransformTable', 'minimumGroupingDigits',
@@ -122,42 +122,42 @@ class LocalisationCache {
         * Keys for items which consist of associative arrays, which may be merged
         * by a fallback sequence.
         */
-       static public $mergeableMapKeys = [ 'messages', 'namespaceNames',
+       public static $mergeableMapKeys = [ 'messages', 'namespaceNames',
                'namespaceAliases', 'dateFormats', 'imageFiles', 'preloadedMessages'
        ];
 
        /**
         * Keys for items which are a numbered array.
         */
-       static public $mergeableListKeys = [ 'extraUserToggles' ];
+       public static $mergeableListKeys = [ 'extraUserToggles' ];
 
        /**
         * Keys for items which contain an array of arrays of equivalent aliases
         * for each subitem. The aliases may be merged by a fallback sequence.
         */
-       static public $mergeableAliasListKeys = [ 'specialPageAliases' ];
+       public static $mergeableAliasListKeys = [ 'specialPageAliases' ];
 
        /**
         * Keys for items which contain an associative array, and may be merged if
         * the primary value contains the special array key "inherit". That array
         * key is removed after the first merge.
         */
-       static public $optionalMergeKeys = [ 'bookstoreList' ];
+       public static $optionalMergeKeys = [ 'bookstoreList' ];
 
        /**
         * Keys for items that are formatted like $magicWords
         */
-       static public $magicWordKeys = [ 'magicWords' ];
+       public static $magicWordKeys = [ 'magicWords' ];
 
        /**
         * Keys for items where the subitems are stored in the backend separately.
         */
-       static public $splitKeys = [ 'messages' ];
+       public static $splitKeys = [ 'messages' ];
 
        /**
         * Keys which are loaded automatically by initLanguage()
         */
-       static public $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
+       public static $preloadedKeys = [ 'dateFormats', 'namespaceNames' ];
 
        /**
         * Associative array of cached plural rules. The key is the language code,
index 5bb1db9..370212b 100644 (file)
@@ -161,7 +161,7 @@ class ChangesListBooleanFilter extends ChangesListFilter {
         * @inheritDoc
         */
        public function displaysOnUnstructuredUi() {
-               return !!$this->showHide;
+               return (bool)$this->showHide;
        }
 
        /**
index deec915..ec86307 100644 (file)
@@ -429,7 +429,7 @@ abstract class ChangesListFilterGroup {
         * @return bool
         */
        public function anySelected( FormOptions $opts ) {
-               return !!count( array_filter(
+               return (bool)count( array_filter(
                        $this->getFilters(),
                        function ( ChangesListFilter $filter ) use ( $opts ) {
                                return $filter->isSelected( $opts );
index a1cf468..6ebe800 100644 (file)
@@ -141,11 +141,11 @@ class ChangeTags {
         * we consider the tag hidden, and return false.
         *
         * @param string $tag
-        * @param IContextSource $context
+        * @param MessageLocalizer $context
         * @return string|bool Tag description or false if tag is to be hidden.
         * @since 1.25 Returns false if tag is to be hidden.
         */
-       public static function tagDescription( $tag, IContextSource $context ) {
+       public static function tagDescription( $tag, MessageLocalizer $context ) {
                $msg = $context->msg( "tag-$tag" );
                if ( !$msg->exists() ) {
                        // No such message, so return the HTML-escaped tag name.
@@ -168,11 +168,11 @@ class ChangeTags {
         * for the long description.
         *
         * @param string $tag
-        * @param IContextSource $context
+        * @param MessageLocalizer $context
         * @return Message|bool Message object of the tag long description or false if
         *  there is no description.
         */
-       public static function tagLongDescriptionMessage( $tag, IContextSource $context ) {
+       public static function tagLongDescriptionMessage( $tag, MessageLocalizer $context ) {
                $msg = $context->msg( "tag-$tag-description" );
                if ( !$msg->exists() ) {
                        return false;
@@ -196,6 +196,8 @@ class ChangeTags {
         * @return string Truncated long tag description.
         */
        public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
+               // FIXME: Make this accept MessageLocalizer and Language instead of IContextSource
+
                $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
                // If there is no tag description, return empty string
                if ( !$originalDesc ) {
index 6977172..7e1f1d1 100644 (file)
@@ -72,7 +72,6 @@ class ChangeTagsLogList extends ChangeTagsList {
         * @return Status
         */
        public function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, $reason, $user ) {
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $this->reset(); $this->current(); $this->next() ) {
                        $item = $this->current();
                        $status = ChangeTags::updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
index 19b7e20..2aad2db 100644 (file)
@@ -80,7 +80,6 @@ class ChangeTagsRevisionList extends ChangeTagsList {
         * @return Status
         */
        public function updateChangeTagsOnAll( $tagsToAdd, $tagsToRemove, $params, $reason, $user ) {
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $this->reset(); $this->current(); $this->next() ) {
                        $item = $this->current();
                        $status = ChangeTags::updateTagsWithChecks( $tagsToAdd, $tagsToRemove,
index 0f8a9a9..2cd1fb3 100644 (file)
@@ -28,17 +28,6 @@ class JsonContent extends TextContent {
                parent::__construct( $text, $modelId );
        }
 
-       /**
-        * Decodes the JSON into a PHP associative array.
-        *
-        * @deprecated since 1.25 Use getData instead.
-        * @return array|null
-        */
-       public function getJsonData() {
-               wfDeprecated( __METHOD__, '1.25' );
-               return FormatJson::decode( $this->getText(), true );
-       }
-
        /**
         * Decodes the JSON string.
         *
index 16c304c..2cbe67c 100644 (file)
@@ -257,6 +257,8 @@ class RequestContext implements IContextSource, MutableContext {
         */
        public function setUser( User $user ) {
                $this->user = $user;
+               // Invalidate cached user interface language
+               $this->lang = null;
        }
 
        /**
index 9f63ede..bbcd33a 100644 (file)
@@ -327,7 +327,7 @@ class LegacyLogger extends AbstractLogger {
                $date = $d->format( 'D M j G:i:s T Y' );
 
                $host = wfHostname();
-               $wiki = WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() );
+               $wiki = WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() );
 
                $text = "{$date}\t{$host}\t{$wiki}\t{$message}\n";
                return $text;
@@ -343,7 +343,7 @@ class LegacyLogger extends AbstractLogger {
         */
        protected static function formatAsWfDebugLog( $channel, $message, $context ) {
                $time = wfTimestamp( TS_DB );
-               $wiki = WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() );
+               $wiki = WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() );
                $host = wfHostname();
                $text = "{$time} {$host} {$wiki}: {$message}\n";
                return $text;
index cb95be6..dc3c1f2 100644 (file)
@@ -38,7 +38,7 @@ class WikiProcessor {
        public function __invoke( array $record ) {
                global $wgVersion;
                $record['extra']['host'] = wfHostname();
-               $record['extra']['wiki'] = WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() );
+               $record['extra']['wiki'] = WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() );
                $record['extra']['mwversion'] = $wgVersion;
                $record['extra']['reqId'] = \WebRequest::getRequestId();
                if ( wfIsCLI() && isset( $_SERVER['argv'] ) ) {
index 301c4f3..5329ca7 100644 (file)
@@ -286,10 +286,3 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate {
                return false;
        }
 }
-
-/**
- * @deprecated since 1.27
- */
-class SquidUpdate extends CdnCacheUpdate {
-       // Keep class name for b/c
-}
index 5ab83c6..0743dbe 100644 (file)
@@ -225,7 +225,7 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
 
        public function getAsJobSpecification() {
                return [
-                       'wiki' => WikiMap::getWikiIdFromDomain( $this->getDB()->getDomainID() ),
+                       'wiki' => WikiMap::getWikiIdFromDbDomain( $this->getDB()->getDomainID() ),
                        'job'  => new JobSpecification(
                                'deleteLinks',
                                [ 'pageId' => $this->pageId, 'timestamp' => $this->timestamp ],
index b4863f8..7c7cabd 100644 (file)
@@ -84,6 +84,16 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
         */
        private $linkDeletions = null;
 
+       /**
+        * @var null|array Added external links if calculated.
+        */
+       private $externalLinkInsertions = null;
+
+       /**
+        * @var null|array Deleted external links if calculated.
+        */
+       private $externalLinkDeletions = null;
+
        /**
         * @var null|array Added properties if calculated.
         */
@@ -234,11 +244,14 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
 
                # External links
                $existingEL = $this->getExistingExternals();
+               $this->externalLinkDeletions = $this->getExternalDeletions( $existingEL );
+               $this->externalLinkInsertions = $this->getExternalInsertions(
+                       $existingEL );
                $this->incrTableUpdate(
                        'externallinks',
                        'el',
-                       $this->getExternalDeletions( $existingEL ),
-                       $this->getExternalInsertions( $existingEL ) );
+                       $this->externalLinkDeletions,
+                       $this->externalLinkInsertions );
 
                # Language links
                $existingLL = $this->getExistingInterlangs();
@@ -1099,6 +1112,36 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
                return $result;
        }
 
+       /**
+        * Fetch external links added by this LinksUpdate. Only available after
+        * the update is complete.
+        * @since 1.33
+        * @return null|array Array of Strings
+        */
+       public function getAddedExternalLinks() {
+               if ( $this->externalLinkInsertions === null ) {
+                       return null;
+               }
+               $result = [];
+               foreach ( $this->externalLinkInsertions as $key => $value ) {
+                       $result[] = $value['el_to'];
+               }
+               return $result;
+       }
+
+       /**
+        * Fetch external links removed by this LinksUpdate. Only available after
+        * the update is complete.
+        * @since 1.33
+        * @return null|array Array of Strings
+        */
+       public function getRemovedExternalLinks() {
+               if ( $this->externalLinkDeletions === null ) {
+                       return null;
+               }
+               return array_keys( $this->externalLinkDeletions );
+       }
+
        /**
         * Fetch page properties added by this LinksUpdate.
         * Only available after the update is complete.
@@ -1162,7 +1205,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
                }
 
                return [
-                       'wiki' => WikiMap::getWikiIdFromDomain( $this->getDB()->getDomainID() ),
+                       'wiki' => WikiMap::getWikiIdFromDbDomain( $this->getDB()->getDomainID() ),
                        'job'  => new JobSpecification(
                                'refreshLinksPrioritized',
                                [
index 8540756..40521d5 100644 (file)
@@ -1655,7 +1655,7 @@ class DifferenceEngine extends ContextSource {
                        RevisionRecord::FOR_THIS_USER, $this->getUser() );
 
                $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
-               $this->mTextLoaded = !!$oldRevision + 1;
+               $this->mTextLoaded = $oldRevision ? 2 : 1;
                $this->isContentOverridden = false;
                $this->slotDiffRenderers = null;
        }
diff --git a/includes/exception/ILocalizedException.php b/includes/exception/ILocalizedException.php
new file mode 100644 (file)
index 0000000..048da42
--- /dev/null
@@ -0,0 +1,33 @@
+<?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
+ *
+ * @file
+ */
+
+/**
+ * Interface for MediaWiki-localized exceptions
+ *
+ * @since 1.29
+ * @ingroup Exception
+ */
+interface ILocalizedException {
+       /**
+        * Return a Message object for this exception
+        * @return Message
+        */
+       public function getMessageObject();
+}
index c0182d5..082d665 100644 (file)
  * @file
  */
 
-/**
- * Interface for MediaWiki-localized exceptions
- *
- * @since 1.29
- * @ingroup Exception
- */
-interface ILocalizedException {
-       /**
-        * Return a Message object for this exception
-        * @return Message
-        */
-       public function getMessageObject();
-}
-
 /**
  * Basic localized exception.
  *
index 951498e..6e3fa79 100644 (file)
@@ -462,22 +462,6 @@ TXT;
                }, $trace );
        }
 
-       /**
-        * Get the ID for this exception.
-        *
-        * The ID is saved so that one can match the one output to the user (when
-        * $wgShowExceptionDetails is set to false), to the entry in the debug log.
-        *
-        * @since 1.22
-        * @deprecated since 1.27: Exception IDs are synonymous with request IDs.
-        * @param Exception|Throwable $e
-        * @return string
-        */
-       public static function getLogId( $e ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               return WebRequest::getRequestId();
-       }
-
        /**
         * If the exception occurred in the course of responding to a request,
         * returns the requested URL. Otherwise, returns false.
index b0516b6..fb6b2b8 100644 (file)
@@ -101,7 +101,7 @@ class BaseDump {
        }
 
        /**
-        * @access private
+        * @private
         */
        function nextPage() {
                if ( $this->skipTo( 'page', 'mediawiki' ) ) {
@@ -121,7 +121,7 @@ class BaseDump {
        }
 
        /**
-        * @access private
+        * @private
         */
        function nextRev() {
                if ( $this->skipTo( 'revision' ) ) {
@@ -134,7 +134,7 @@ class BaseDump {
        }
 
        /**
-        * @access private
+        * @private
         * @return string
         */
        function nextText() {
@@ -144,7 +144,7 @@ class BaseDump {
        }
 
        /**
-        * @access private
+        * @private
         * @param string $name
         * @param string $parent
         * @return bool|null
@@ -177,7 +177,7 @@ class BaseDump {
         * no sub-elements or such scary things.
         *
         * @return string
-        * @access private
+        * @private
         */
        function nodeContents() {
                if ( $this->atEnd ) {
@@ -203,7 +203,7 @@ class BaseDump {
        }
 
        /**
-        * @access private
+        * @private
         * @return null
         */
        function close() {
index e6f1fd7..fae8b62 100644 (file)
@@ -186,7 +186,7 @@ class XmlDumpWriter {
        /**
         * Closes a "<page>" section on the output stream.
         *
-        * @access private
+        * @private
         * @return string
         */
        function closePage() {
@@ -199,7 +199,7 @@ class XmlDumpWriter {
         *
         * @param object $row
         * @return string
-        * @access private
+        * @private
         */
        function writeRevision( $row ) {
                $out = "    <revision>\n";
@@ -289,7 +289,7 @@ class XmlDumpWriter {
         *
         * @param object $row
         * @return string
-        * @access private
+        * @private
         */
        function writeLogItem( $row ) {
                $out = "  <logitem>\n";
index b8fb7fd..aa955d0 100644 (file)
@@ -51,7 +51,7 @@ class LockManagerGroup {
         */
        public static function singleton( $domain = false ) {
                if ( $domain === false ) {
-                       $domain = WikiMap::getCurrentWikiDomain()->getId();
+                       $domain = WikiMap::getCurrentWikiDbDomain()->getId();
                }
 
                if ( !isset( self::$instances[$domain] ) ) {
index 878e82d..789d7c5 100644 (file)
@@ -1431,7 +1431,7 @@ class LocalFile extends File {
                $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = [],
                $createNullRevision = true
        ) {
-               global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
+               global $wgActorTableSchemaMigrationStage;
 
                if ( is_null( $user ) ) {
                        global $wgUser;
@@ -1528,6 +1528,7 @@ class LocalFile extends File {
                                'oi_width' => 'img_width',
                                'oi_height' => 'img_height',
                                'oi_bits' => 'img_bits',
+                               'oi_description_id' => 'img_description_id',
                                'oi_timestamp' => 'img_timestamp',
                                'oi_metadata' => 'img_metadata',
                                'oi_media_type' => 'img_media_type',
@@ -1537,39 +1538,6 @@ class LocalFile extends File {
                        ];
                        $joins = [];
 
-                       if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
-                               $fields['oi_description'] = 'img_description';
-                       }
-                       if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
-                               $fields['oi_description_id'] = 'img_description_id';
-                       }
-
-                       if ( $wgCommentTableSchemaMigrationStage !== MIGRATION_OLD &&
-                               $wgCommentTableSchemaMigrationStage !== MIGRATION_NEW
-                       ) {
-                               // Upgrade any rows that are still old-style. Otherwise an upgrade
-                               // might be missed if a deletion happens while the migration script
-                               // is running.
-                               $res = $dbw->select(
-                                       [ 'image' ],
-                                       [ 'img_name', 'img_description' ],
-                                       [
-                                               'img_name' => $this->getName(),
-                                               'img_description_id' => 0,
-                                       ],
-                                       __METHOD__
-                               );
-                               foreach ( $res as $row ) {
-                                       $imgFields = $commentStore->insert( $dbw, 'img_description', $row->img_description );
-                                       $dbw->update(
-                                               'image',
-                                               $imgFields,
-                                               [ 'img_name' => $row->img_name ],
-                                               __METHOD__
-                                       );
-                               }
-                       }
-
                        if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                $fields['oi_user'] = 'img_user';
                                $fields['oi_user_text'] = 'img_user_text';
@@ -2470,7 +2438,7 @@ class LocalFileDeleteBatch {
        }
 
        protected function doDBInserts() {
-               global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
+               global $wgActorTableSchemaMigrationStage;
 
                $now = time();
                $dbw = $this->file->repo->getMasterDB();
@@ -2515,6 +2483,7 @@ class LocalFileDeleteBatch {
                                'fa_media_type' => 'img_media_type',
                                'fa_major_mime' => 'img_major_mime',
                                'fa_minor_mime' => 'img_minor_mime',
+                               'fa_description_id' => 'img_description_id',
                                'fa_timestamp' => 'img_timestamp',
                                'fa_sha1' => 'img_sha1'
                        ];
@@ -2525,39 +2494,6 @@ class LocalFileDeleteBatch {
                                $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
                        );
 
-                       if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
-                               $fields['fa_description'] = 'img_description';
-                       }
-                       if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
-                               $fields['fa_description_id'] = 'img_description_id';
-                       }
-
-                       if ( $wgCommentTableSchemaMigrationStage !== MIGRATION_OLD &&
-                               $wgCommentTableSchemaMigrationStage !== MIGRATION_NEW
-                       ) {
-                               // Upgrade any rows that are still old-style. Otherwise an upgrade
-                               // might be missed if a deletion happens while the migration script
-                               // is running.
-                               $res = $dbw->select(
-                                       [ 'image' ],
-                                       [ 'img_name', 'img_description' ],
-                                       [
-                                               'img_name' => $this->file->getName(),
-                                               'img_description_id' => 0,
-                                       ],
-                                       __METHOD__
-                               );
-                               foreach ( $res as $row ) {
-                                       $imgFields = $commentStore->insert( $dbw, 'img_description', $row->img_description );
-                                       $dbw->update(
-                                               'image',
-                                               $imgFields,
-                                               [ 'img_name' => $row->img_name ],
-                                               __METHOD__
-                                       );
-                               }
-                       }
-
                        if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
                                $fields['fa_user'] = 'img_user';
                                $fields['fa_user_text'] = 'img_user_text';
index 6e8c8c9..06e1271 100644 (file)
@@ -89,7 +89,7 @@ abstract class ImageGalleryBase extends ContextSource {
        protected $mAttribs = [];
 
        /** @var bool */
-       static private $modeMapping = false;
+       private static $modeMapping = false;
 
        /**
         * Get a new image gallery. This is the method other callers
index 274d2c0..8ce3c83 100644 (file)
@@ -23,7 +23,7 @@
  *       Not used by OOUI form fields.
  */
 class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
-       static private $requiredParams = [
+       private static $requiredParams = [
                // Required by underlying HTMLFormField
                'fieldname',
                // Required by HTMLCheckMatrix
index ef70467..e6b2892 100644 (file)
@@ -95,7 +95,7 @@ class GuzzleHttpRequest extends MWHttpRequest {
         * If a sink is already specified, this does nothing.  This causes the 'sink' constructor
         * option to override the 'callback' constructor option.
         *
-        * @param $callback|null $callback
+        * @param callable|null $callback
         * @throws InvalidArgumentException
         */
        protected function doSetCallback( $callback ) {
index eaf2763..c29f199 100644 (file)
@@ -25,7 +25,7 @@ use MediaWiki\Logger\LoggerFactory;
  * @ingroup HTTP
  */
 class Http {
-       static public $httpEngine = false;
+       public static $httpEngine = false;
 
        /**
         * Perform an HTTP request
index 0faef17..a3a14d0 100644 (file)
@@ -44,7 +44,7 @@ class HttpRequestFactory {
         */
        public function create( $url, array $options = [], $caller = __METHOD__ ) {
                if ( !Http::$httpEngine ) {
-                       Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
+                       Http::$httpEngine = 'guzzle';
                } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
                        throw new DomainException( __METHOD__ . ': curl (https://secure.php.net/curl) is not ' .
                           'installed, but Http::$httpEngine is set to "curl"' );
index 3b56f21..b4ac9a7 100644 (file)
@@ -129,6 +129,8 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
                        $this->setOriginalRequest( $options['originalRequest'] );
                }
 
+               $this->setHeader( 'X-Request-Id', WebRequest::getRequestId() );
+
                $members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
                                "method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
 
@@ -333,7 +335,7 @@ abstract class MWHttpRequest implements LoggerAwareInterface {
         * Worker function for setting callbacks.  Calls can originate both internally and externally
         * via setCallback).  Defaults to the internal read callback if $callback is null.
         *
-        * @param $callback|null $callback
+        * @param callable|null $callback
         * @throws InvalidArgumentException
         */
        protected function doSetCallback( $callback ) {
index 9098981..4d72102 100644 (file)
@@ -536,7 +536,7 @@ class WikiImporter {
         * Fetches text contents of the current element, assuming
         * no sub-elements or such scary things.
         * @return string
-        * @access private
+        * @private
         */
        public function nodeContents() {
                if ( $this->reader->isEmptyElement ) {
index 43000b8..d64e2d7 100644 (file)
@@ -1261,10 +1261,7 @@ abstract class DatabaseUpdater {
         * @since 1.30
         */
        protected function migrateComments() {
-               global $wgCommentTableSchemaMigrationStage;
-               if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
-                       !$this->updateRowExists( 'MigrateComments' )
-               ) {
+               if ( !$this->updateRowExists( 'MigrateComments' ) ) {
                        $this->output(
                                "Migrating comments to the 'comments' table, printing progress markers. For large\n" .
                                "databases, you may want to hit Ctrl-C and do this manually with\n" .
@@ -1281,20 +1278,14 @@ abstract class DatabaseUpdater {
         * @since 1.32
         */
        protected function migrateImageCommentTemp() {
-               global $wgCommentTableSchemaMigrationStage;
-
                if ( $this->tableExists( 'image_comment_temp' ) ) {
-                       if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
-                               $this->output( "Merging image_comment_temp into the image table\n" );
-                               $task = $this->maintenance->runChild(
-                                       MigrateImageCommentTemp::class, 'migrateImageCommentTemp.php'
-                               );
-                               $task->setForce();
-                               $ok = $task->execute();
-                               $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
-                       } else {
-                               $ok = true;
-                       }
+                       $this->output( "Merging image_comment_temp into the image table\n" );
+                       $task = $this->maintenance->runChild(
+                               MigrateImageCommentTemp::class, 'migrateImageCommentTemp.php'
+                       );
+                       $task->setForce();
+                       $ok = $task->execute();
+                       $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
                        if ( $ok ) {
                                $this->dropTable( 'image_comment_temp' );
                        }
index 2f1c0f3..75f3894 100644 (file)
@@ -156,6 +156,7 @@ class MssqlUpdater extends DatabaseUpdater {
                        [ 'dropField', 'change_tag', 'ct_tag', 'patch-drop-ct_tag.sql' ],
                        [ 'dropTable', 'valid_tag' ],
                        [ 'dropTable', 'tag_summary' ],
+                       [ 'dropField', 'protected_titles', 'pt_reason', 'patch-drop-comment-fields.sql' ],
                ];
        }
 
index f7362cb..2e3fdba 100644 (file)
@@ -374,6 +374,7 @@ class MysqlUpdater extends DatabaseUpdater {
                        [ 'dropField', 'change_tag', 'ct_tag', 'patch-drop-ct_tag.sql' ],
                        [ 'dropTable', 'valid_tag' ],
                        [ 'dropTable', 'tag_summary' ],
+                       [ 'dropField', 'protected_titles', 'pt_reason', 'patch-drop-comment-fields.sql' ],
                ];
        }
 
index e9be744..d3a7b13 100644 (file)
@@ -167,6 +167,7 @@ class OracleUpdater extends DatabaseUpdater {
                        [ 'dropField', 'change_tag', 'ct_tag', 'patch-drop-ct_tag.sql' ],
                        [ 'dropTable', 'valid_tag' ],
                        [ 'dropTable', 'tag_summary' ],
+                       [ 'dropField', 'protected_titles', 'pt_reason', 'patch-drop-comment-fields.sql' ],
 
                        // KEEP THIS AT THE BOTTOM!!
                        [ 'doRebuildDuplicateFunction' ],
index 7c0d3e3..75877a1 100644 (file)
@@ -600,6 +600,24 @@ class PostgresUpdater extends DatabaseUpdater {
                        [ 'dropField', 'change_tag', 'ct_tag', 'patch-drop-ct_tag.sql' ],
                        [ 'dropTable', 'valid_tag' ],
                        [ 'dropTable', 'tag_summary' ],
+                       [ 'dropPgField', 'archive', 'ar_comment' ],
+                       [ 'dropDefault', 'archive', 'ar_comment_id' ],
+                       [ 'dropPgField', 'ipblocks', 'ipb_reason' ],
+                       [ 'dropDefault', 'ipblocks', 'ipb_reason_id' ],
+                       [ 'dropPgField', 'image', 'img_description' ],
+                       [ 'dropDefault', 'image', 'img_description_id' ],
+                       [ 'dropPgField', 'oldimage', 'oi_description' ],
+                       [ 'dropDefault', 'oldimage', 'oi_description_id' ],
+                       [ 'dropPgField', 'filearchive', 'fa_deleted_reason' ],
+                       [ 'dropDefault', 'filearchive', 'fa_deleted_reason_id' ],
+                       [ 'dropPgField', 'filearchive', 'fa_description' ],
+                       [ 'dropDefault', 'filearchive', 'fa_description_id' ],
+                       [ 'dropPgField', 'recentchanges', 'rc_comment' ],
+                       [ 'dropDefault', 'recentchanges', 'rc_comment_id' ],
+                       [ 'dropPgField', 'logging', 'log_comment' ],
+                       [ 'dropDefault', 'logging', 'log_comment_id' ],
+                       [ 'dropPgField', 'protected_titles', 'pt_reason' ],
+                       [ 'dropDefault', 'protected_titles', 'pt_reason_id' ],
                ];
        }
 
@@ -928,7 +946,7 @@ END;
 
        protected function setDefault( $table, $field, $default ) {
                $info = $this->db->fieldInfo( $table, $field );
-               if ( $info->defaultValue() !== $default ) {
+               if ( $info && $info->defaultValue() !== $default ) {
                        $this->output( "Changing '$table.$field' default value\n" );
                        $this->db->query( "ALTER TABLE $table ALTER $field SET DEFAULT "
                                . $this->db->addQuotes( $default ) );
index 1d99b87..17ced50 100644 (file)
@@ -241,6 +241,14 @@ class SqliteUpdater extends DatabaseUpdater {
                        [ 'dropField', 'change_tag', 'ct_tag', 'patch-drop-ct_tag.sql' ],
                        [ 'dropTable', 'valid_tag' ],
                        [ 'dropTable', 'tag_summary' ],
+                       [ 'dropField', 'archive', 'ar_comment', 'patch-archive-drop-ar_comment.sql' ],
+                       [ 'dropField', 'ipblocks', 'ipb_reason', 'patch-ipblocks-drop-ipb_reason.sql' ],
+                       [ 'dropField', 'image', 'img_description', 'patch-image-drop-img_description.sql' ],
+                       [ 'dropField', 'oldimage', 'oi_description', 'patch-oldimage-drop-oi_description.sql' ],
+                       [ 'dropField', 'filearchive', 'fa_description', 'patch-filearchive-drop-fa_description.sql' ],
+                       [ 'dropField', 'recentchanges', 'rc_comment', 'patch-recentchanges-drop-rc_comment.sql' ],
+                       [ 'dropField', 'logging', 'log_comment', 'patch-logging-drop-log_comment.sql' ],
+                       [ 'dropField', 'protected_titles', 'pt_reason', 'patch-protected_titles-drop-pt_reason.sql' ],
                ];
        }
 
index 8a00855..eaa5914 100644 (file)
@@ -85,7 +85,7 @@
        "config-using-32bit": "<strong>Папярэджаньне:</strong> падобна, што вашая сыстэма выкарыстоўвае 32-бітавыя цэлыя лікі. Гэта [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit не рэкамэндуецца].",
        "config-db-type": "Тып базы зьвестак:",
        "config-db-host": "Хост базы зьвестак:",
-       "config-db-host-help": "Ð\9aалÑ\96 Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80 Ð\92аÑ\88ай Ð±Ð°Ð·Ñ\8b Ð·Ñ\8cвеÑ\81Ñ\82ак Ð·Ð½Ð°Ñ\85одзÑ\96Ñ\86Ñ\86а Ð½Ð° Ñ\96нÑ\88Ñ\8bм Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80Ñ\8b, Ñ\83вÑ\8fдзÑ\96Ñ\86е Ñ\82Ñ\83Ñ\82 Ñ\96мÑ\8f Ñ\85оÑ\81Ñ\82а Ñ\86Ñ\96 IP-адÑ\80аÑ\81.\n\nÐ\9aалÑ\96 Ð\92Ñ\8b ÐºÐ°Ñ\80Ñ\8bÑ\81Ñ\82аеÑ\86еÑ\81Ñ\8f shared-Ñ\85оÑ\81Ñ\82Ñ\8bнгам, Ð\92аÑ\88 Ñ\85оÑ\81Ñ\82Ñ\8bнг-пÑ\80авайдÑ\8dÑ\80 Ð¼Ñ\83Ñ\81Ñ\96Ñ\86Ñ\8c Ð´Ð°Ñ\86Ñ\8c Ð\92ам Ñ\81лÑ\83Ñ\88нае Ñ\96мÑ\8f Ñ\85оÑ\81Ñ\82а Ð±Ð°Ð·Ñ\8b Ð·Ñ\8cвеÑ\81Ñ\82ак Ñ\83 Ñ\81ваÑ\91й Ð´Ð°ÐºÑ\83мÑ\8dнÑ\82аÑ\86Ñ\8bÑ\96.\n\nÐ\9aалÑ\96 Ð\92Ñ\8b Ñ\83Ñ\81Ñ\82алÑ\91Ñ\9eваеÑ\86е Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80 Windows Ð· Ð²Ñ\8bкаÑ\80Ñ\8bÑ\81Ñ\82анÑ\8cнем MySQL, Ð²Ñ\8bкаÑ\80Ñ\8bÑ\81Ñ\82анÑ\8cне Â«localhost» Ð¼Ð¾Ð¶Ð° Ð½Ðµ Ð¿Ñ\80аÑ\86аваÑ\86Ñ\8c Ð´Ð»Ñ\8f Ð½Ð°Ð·Ð²Ñ\8b Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80а. Ð£ Ð³Ñ\8dÑ\82Ñ\8bм Ð²Ñ\8bпадкÑ\83 Ð¿Ð°Ñ\81пÑ\80абÑ\83йÑ\86е Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bÑ\86Ñ\8c Â«127.0.0.1»  Ð´Ð»Ñ\8f Ð»Ñ\8fкалÑ\8cнага IP-адÑ\80аÑ\81а.\n\nÐ\9aалÑ\96 Ð\92ы выкарыстоўваеце PostgreSQL, пакіньце поле пустым, каб далучыцца праз Unix-сокет.",
+       "config-db-host-help": "Ð\9aалÑ\96 Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80 Ð²Ð°Ñ\88ай Ð±Ð°Ð·Ñ\8b Ð·Ñ\8cвеÑ\81Ñ\82ак Ð·Ð½Ð°Ñ\85одзÑ\96Ñ\86Ñ\86а Ð½Ð° Ñ\96нÑ\88Ñ\8bм Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80Ñ\8b, Ñ\83вÑ\8fдзÑ\96Ñ\86е Ñ\82Ñ\83Ñ\82 Ñ\96мÑ\8f Ñ\85оÑ\81Ñ\82а Ñ\86Ñ\96 IP-адÑ\80аÑ\81.\n\nÐ\9aалÑ\96 Ð²Ñ\8b ÐºÐ°Ñ\80Ñ\8bÑ\81Ñ\82аеÑ\86еÑ\81Ñ\8f shared-Ñ\85оÑ\81Ñ\82Ñ\8bнгам, Ð²Ð°Ñ\88 Ñ\85оÑ\81Ñ\82Ñ\8bнг-пÑ\80авайдÑ\8dÑ\80 Ð¼Ñ\83Ñ\81Ñ\96Ñ\86Ñ\8c Ð´Ð°Ñ\86Ñ\8c Ð²Ð°Ð¼ Ñ\81лÑ\83Ñ\88нае Ñ\96мÑ\8f Ñ\85оÑ\81Ñ\82а Ð±Ð°Ð·Ñ\8b Ð·Ñ\8cвеÑ\81Ñ\82ак Ñ\83 Ñ\81ваÑ\91й Ð´Ð°ÐºÑ\83мÑ\8dнÑ\82аÑ\86Ñ\8bÑ\96.\n\nÐ\9aалÑ\96 Ð²Ñ\8b Ñ\9eжÑ\8bваеÑ\86е MySQL, Ð²Ñ\8bкаÑ\80Ñ\8bÑ\81Ñ\82анÑ\8cне Â«localhost» Ð¼Ð¾Ð¶Ð° Ð½Ðµ Ð¿Ñ\80аÑ\86аваÑ\86Ñ\8c Ð´Ð»Ñ\8f Ð½Ð°Ð·Ð²Ñ\8b Ñ\81Ñ\8dÑ\80вÑ\8dÑ\80а. Ð£ Ð³Ñ\8dÑ\82Ñ\8bм Ð²Ñ\8bпадкÑ\83 Ð¿Ð°Ñ\81пÑ\80абÑ\83йÑ\86е Ð¿Ð°Ð·Ð½Ð°Ñ\87Ñ\8bÑ\86Ñ\8c Â«127.0.0.1» Ð´Ð»Ñ\8f Ð»Ñ\8fкалÑ\8cнага IP-адÑ\80аÑ\81Ñ\83.\n\nÐ\9aалÑ\96 Ð²ы выкарыстоўваеце PostgreSQL, пакіньце поле пустым, каб далучыцца праз Unix-сокет.",
        "config-db-host-oracle": "TNS базы зьвестак:",
        "config-db-host-oracle-help": "Увядзіце слушнае [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm лякальнае імя злучэньня]; файл tnsnames.ora павінен быць бачным для гэтага ўсталяваньня.<br />Калі Вы выкарыстоўваеце кліенцкія бібліятэкі 10g ці больш новыя, Вы можаце таксама выкарыстоўваць мэтад наданьня назваў [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm лёгкае злучэньне].",
        "config-db-wiki-settings": "Ідэнтыфікацыя гэтай вікі",
index 4533fa3..9df3c8b 100644 (file)
@@ -55,6 +55,7 @@
        "config-using-uri": "ব্যবহার করা সার্ভারের ইউআরএল \"<nowiki>$1$2</nowiki>\"।",
        "config-db-type": "ডেটাবেসের ধরন:",
        "config-db-host": "ডেটাবেজের হোস্ট:",
+       "config-db-host-help": "যদি আপনার ডাটাবেস সার্ভার ভিন্ন সার্ভারে থাকে, তবে এখানে হোস্ট নাম বা আইপি ঠিকানা প্রবেশ করান।\n\nআপনি যদি ভাগকৃত ওয়েব হোস্টিং ব্যবহার করেন, তবে আপনার হোস্টিং প্রদানকারী তাঁদের নথিপত্রে আপনাকে সঠিক হোস্ট নামটি দিয়ে থাকবে।\n\nযদি আপনি MySQL ব্যবহার করেন, তবে সার্ভারের নামটির জন্য \"localhost\" ব্যবহার কাজ নাও করতে পারে। যদি এটি না হয়, তবে স্থানীয় আইপি ঠিকানার জন্য \"127.0.0.1\" দিয়ে চেষ্টা করুন।\n\nযদি আপনি PostgreSQL ব্যবহার করেন, ইউনিক্স সকেটের মাধ্যমে সংযোগ করতে এই ক্ষেত্রটি ফাঁকা রাখুন।",
        "config-db-wiki-settings": "এই উইকি সনাক্ত করুন",
        "config-db-name": "ডেটাবেসের নাম (হাইফেন ছাড়া):",
        "config-db-install-account": "ইন্সটলের জন্য ব্যবহারকারী অ্যাকাউন্ট",
index bddfb0a..b618f7a 100644 (file)
@@ -89,7 +89,7 @@
        "config-using-32bit": "<strong>Upozornění:</strong> Vypadá to, že váš systém běží s 32bitovými celými čísly. To [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit se nedoporučuje].",
        "config-db-type": "Typ databáze:",
        "config-db-host": "Databázový server:",
-       "config-db-host-help": "Pokud je váš databázový server na jiném počítači, zadejte zde jméno stroje nebo IP adresu.\n\nPokud používáte sdílený webový hosting, váš poskytovatel by vám měl v dokumentaci sdělit správné jméno stroje.\n\nPokud instalujete na server běžící na Windows a používáte MySQL, jméno „localhost“ nemusí fungovat. V takovém případě zkuste jako místní IP adresu zadat „127.0.0.1“.\n\nPokud používáte PostgreSQL, můžete se připojit Unixovými sockety tak, že toto pole necháte prázdné.",
+       "config-db-host-help": "Pokud je váš databázový server na jiném počítači, zadejte zde jméno stroje nebo IP adresu.\n\nPokud používáte sdílený webový hosting, váš poskytovatel by vám měl v dokumentaci sdělit správné jméno stroje.\n\nPokud používáte MySQL, jméno „localhost“ nemusí fungovat. V takovém případě zkuste jako místní IP adresu zadat „127.0.0.1“.\n\nPokud používáte PostgreSQL, můžete se připojit Unixovými sockety tak, že toto pole necháte prázdné.",
        "config-db-host-oracle": "Databázové TNS:",
        "config-db-host-oracle-help": "Zadejte platné [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; tato instalace musí vidět soubor tnsnames.ora.<br />Pokud používáte klientské knihovny verze 10g nebo novější, můžete také používat názvy [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].",
        "config-db-wiki-settings": "Identifikace této wiki",
index c87dcba..c248468 100644 (file)
@@ -78,7 +78,7 @@
        "config-using-32bit": "<strong>Warning:</strong> your system appears to be running with 32-bit integers. This is [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit not advised].",
        "config-db-type": "Database type:",
        "config-db-host": "Database host:",
-       "config-db-host-help": "If your database server is on different server, enter the host name or IP address here.\n\nIf you are using shared web hosting, your hosting provider should give you the correct host name in their documentation.\n\nIf you using MySQL, using \"localhost\" may not work for the server name. If it does not, try \"127.0.0.1\" for the local IP address.\n\nIf you are using PostgreSQL, leave this field blank to connect via a Unix socket.",
+       "config-db-host-help": "If your database server is on a different server, enter the host name or IP address here.\n\nIf you are using shared web hosting, your hosting provider should give you the correct host name in their documentation.\n\nIf you are using MySQL, using \"localhost\" may not work for the server name. If it does not, try \"127.0.0.1\" for the local IP address.\n\nIf you are using PostgreSQL, leave this field blank to connect via a Unix socket.",
        "config-db-host-oracle": "Database TNS:",
        "config-db-host-oracle-help": "Enter a valid [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; a tnsnames.ora file must be visible to this installation.<br />If you are using client libraries 10g or newer you can also use the [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] naming method.",
        "config-db-wiki-settings": "Identify this wiki",
index 7ec2934..8048a26 100644 (file)
@@ -37,7 +37,8 @@
                        "KATRINE1992",
                        "MarcoAurelio",
                        "Adjen",
-                       "Dschultz"
+                       "Dschultz",
+                       "Carlosmg.dg"
                ]
        },
        "config-desc": "El instalador de MediaWiki",
        "config-db-host-oracle": "TNS de la base de datos:",
        "config-db-host-oracle-help": "Escribe un [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm nombre de conexión local] válido; un archivo tnsnames.ora debe ser visible para esta instalación.<br />Si estás utilizando bibliotecas de cliente 10g o más recientes también puedes utilizar el método de asignación de nombres [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].",
        "config-db-wiki-settings": "Identifica este wiki",
-       "config-db-name": "Nombre de la base de datos:",
+       "config-db-name": "Nombre de base de datos (ningún guion):",
        "config-db-name-help": "Elige un nombre que identifique tu wiki.\nNo debe contener espacios.\n\nSi estás utilizando alojamiento web compartido, tu proveedor te dará un nombre específico de base de datos para que lo utilices, o bien te permitirá crear bases de datos a través de un panel de control.",
        "config-db-name-oracle": "Esquema de la base de datos:",
        "config-db-account-oracle-warn": "Hay tres escenarios compatibles para la instalación de Oracle como base de datos back-end:\n\nSi desea crear una cuenta de base de datos como parte del proceso de instalación, por favor suministre una cuenta con función SYSDBA como cuenta de base de datos para la instalación y especifique las credenciales deseadas de la cuenta de acceso al web, de lo contrario puede crear manualmente la cuenta de acceso al web y suministrar sólo esa cuenta (si tiene los permisos necesarios para crear los objetos de esquema) o suministrar dos cuentas diferentes, una con privilegios de creación y otra con acceso restringido a la web\n\nLa secuencia de comandos (script) para crear una cuenta con los privilegios necesarios puede encontrarse en el directorio \"maintenance/oracle/\" de esta instalación. Tenga en cuenta que utilizando una cuenta restringida desactivará todas las capacidades de mantenimiento con la cuenta predeterminada.",
        "config-db-account-lock": "Usar el mismo nombre de usuario y contraseña durante operación normal",
        "config-db-wiki-account": "Cuenta de usuario para operación normal",
        "config-db-wiki-help": "Escribe el nombre de usuario y la contraseña que se utilizarán para acceder a la base de datos durante la operación normal del wiki.\nSi esta cuenta no existe y la cuenta de instalación tiene suficientes privilegios, se creará esta cuenta de usuario con los privilegios mínimos necesarios para la operación normal del wiki.",
-       "config-db-prefix": "Prefijo de tablas de la base de datos:",
+       "config-db-prefix": "Prefijo de tabla de la base de datos (ningún guion):",
        "config-db-prefix-help": "Si necesitas compartir una base de datos entre múltiples wikis, o entre MediaWiki y otra aplicación web, puedes optar por agregar un prefijo a todos los nombres de tabla para evitar conflictos.\nNo utilices espacios.\n\nNormalmente se deja este campo vacío.",
        "config-mysql-old": "Se necesita MySQL $1 o posterior. Tienes $2.",
        "config-db-port": "Puerto de la base de datos:",
-       "config-db-schema": "Esquema para MediaWiki",
+       "config-db-schema": "Esquema para MediaWiki (ningún guion):",
        "config-db-schema-help": "Este esquema usualmente estará bien.\nCámbialos solo si lo necesitas.",
        "config-pg-test-error": "No se puede conectar con la base de datos <strong>$1</strong>: $2",
        "config-sqlite-dir": "Directorio de datos SQLite:",
index 8224bd2..a134db8 100644 (file)
@@ -95,7 +95,7 @@
        "config-suhosin-max-value-length": "Suhosin on asennettu ja se rajoittaa GET-parametrin <code>length</code> $1 tavuun.\nMediaWikin ResourceLoader-komponentti pystyy toimimaan tämän kanssa, mutta ohjelmiston suorituskyky heikkenee.\nMikäli mahdollista, aseta muuttuja <code>suhosin.get.max_value_length</code> arvoon 1024 (tai suurempaan) tiedostossa <code>php.ini</code> ja aseta myös <code>$wgResourceLoaderMaxQueryLength</code> samaksi arvoksi tiedostossa <code>LocalSettings.php</code>.",
        "config-db-type": "Tietokannan tyyppi:",
        "config-db-host": "Tietokantapalvelin:",
-       "config-db-host-help": "Jos tietokantapalvelimesi sijaitsee eri palvelimella, syötä palvelimen nimi tai ip-osoite tähän.\n\nJos käytössäsi on ulkoinen palveluntarjoaja, pitäisi palvelimen nimen löytyä yrityksen ohjesivuilta.\n\nJos asennat MediaWikiä Windows-palvelimelle ja käytät MySQL:ää ei palvelimen nimi \"localhost\" välttämättä toimi. Tässä tapauksessa koita käyttää osoitetta 127.0.0.1.\n\nJos käytät PostgreSQL:ää jätä tämä kenttä tyhjäksi.",
+       "config-db-host-help": "Jos tietokantapalvelimesi sijaitsee eri palvelimella, syötä palvelimen nimi tai ip-osoite tähän.\n\nJos käytössäsi on ulkoinen palveluntarjoaja, pitäisi palvelimen nimen löytyä yrityksen ohjesivuilta.\n\nJos käytät MySQL:ää, ei palvelimen nimi \"localhost\" välttämättä toimi. Tässä tapauksessa koita käyttää osoitetta 127.0.0.1.\n\nJos käytät PostgreSQL:ää jätä tämä kenttä tyhjäksi.",
        "config-db-host-oracle": "Tietokannan TNS:",
        "config-db-wiki-settings": "Identifioi tämä wiki",
        "config-db-name": "Tietokannan nimi (ei väliviivoja):",
        "config-invalid-db-server-oracle": "Virheellinen tietokanta TNS \"$1\".\nKäytä joko \"TNS Name\"- tai \"Easy Connect\" -tekstiä\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle metodien nimeäminen]).",
        "config-invalid-db-name": "”$1” ei kelpaa tietokannan nimeksi.\nKäytä ainoastaan kirjaimia (a-z, A-Z), numeroita (0-9), alaviivoja (_) ja tavuviivoja (-).",
        "config-invalid-db-prefix": "”$1” ei kelpaa tietokannan etuliitteeksi.\nKäytä ainoastaan kirjaimia (a-z, A-Z), numeroita (0-9), alaviivoja (_) ja tavuviivoja (-).",
-       "config-connection-error": "$1.\n\nTarkista isäntä, käyttäjänimi, salasana ja yritä uudestaan.",
+       "config-connection-error": "$1.\n\nTarkista isäntä, käyttäjänimi, salasana ja yritä uudestaan. Jos käytät \"localhost\" tietokannan isäntänä, kokeile käyttää \"127.0.0.1\" sen sijaan (tai toisinpäin).",
        "config-invalid-schema": "Virheellinen skeema MediaWikille \"$1\".\nKäytä pelkkiä ASCII-kirjaimia (a-z, A-Z), numeroita (0-9) ja alaviivoja (_).",
        "config-db-sys-create-oracle": "Asennusohjelma tukee ainoastaan SYSDBA-tunnuksen käyttämistä uuden tunnuksen luonnissa.",
        "config-db-sys-user-exists-oracle": "Käyttäjätunnus \"$1\" on jo olemassa. SYSDBA:ta voidaan käyttää vain uuden tunnuksen luontiin!",
index 82b2d79..70462a1 100644 (file)
@@ -88,7 +88,7 @@
        "config-mod-security": "<strong>警告:</strong> あなたのウェブサーバーでは [https://modsecurity.org/ mod_security] が有効になっています。正しく構成されていない場合は、MediaWiki や利用者にコンテンツの投稿を許可するその他のソフトウェアに問題が発生する場合があります。\n[https://modsecurity.org/documentation/ mod_security の説明文書]を確認するか、ランダムなエラーが発生した場合はあなたのホストのサポートにお問い合わせください。",
        "config-diff3-bad": "GNU diff3 が見つかりません。",
        "config-git": "バージョン管理ソフトウェア Git が見つかりました: <code>$1</code>",
-       "config-git-bad": "バージョン管理ソフトウェア Git が見つかりません。",
+       "config-git-bad": "バージョン管理ソフトウェア Git が見つかりません。これを無視することもできますが、Special:Version にコミットのハッシュが表示されないことにご注意ください。",
        "config-imagemagick": "ImageMagickが見つかりました: <code>$1</code>。\nアップロードが有効であれば、画像のサムネイルを利用できます。",
        "config-gd": "GD画像ライブラリが内蔵されていることが確認されました。\nアップロードが有効なら、画像のサムネイルが利用できます。",
        "config-no-scaling": "GDライブラリもImageMagickも見つかりませんでした。\n画像のサムネイル生成は無効になります。",
        "config-using-32bit": "<strong>警告:</strong>システムが32ビットで動作しているようです。 これは[https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit 非推奨]です。",
        "config-db-type": "データベースの種類:",
        "config-db-host": "データベースのホスト:",
-       "config-db-host-help": "異なるサーバー上にデータベースサーバーがある場合、ホスト名またはIPアドレスをここに入力してください。\n\nもし、共有されたウェブホスティングを使用している場合、ホスティングプロバイダーは正確なホスト名を解説しているはずです。\n\nWindowsでMySQLを使用している場合に、「localhost」は、サーバー名としてはうまく働かないでしょう。もしそのような場合は、ローカルIPアドレスとして「127.0.0.1」を試してみてください。\n\nPostgreSQLを使用している場合、UNIXソケットで接続するにはこの欄を空欄のままにしてください。",
+       "config-db-host-help": "異なるサーバー上にデータベースサーバーがある場合、ホスト名またはIPアドレスをここに入力してください。\n\nもし、共有されたウェブホスティングを使用している場合、ホスティングプロバイダーは正確なホスト名を解説しているはずです。\n\nMySQLを使用している場合、「localhost」は、サーバー名としてはうまく働かないでしょう。もしそのような場合は、ローカルIPアドレスとして「127.0.0.1」を試してみてください。\n\nPostgreSQLを使用している場合、UNIXソケットで接続するにはこの欄を空欄のままにしてください。",
        "config-db-host-oracle": "データベース TNS:",
        "config-db-host-oracle-help": "有効な[http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm ローカル接続名]を入力してください。tnsnames.ora ファイルは、このインストール先から参照できる場所に置いてください。<br />ご使用中のクライアント ライブラリが 10g 以降の場合、[http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect] ネーミング メソッドを使用できます。",
        "config-db-wiki-settings": "このウィキの識別情報",
index a66c55b..8c19cb1 100644 (file)
        "config-using-32bit": "<strong>Pas op:</strong> uw systeem lijkt met 32-bit integers te werken. Dit is [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit anders dan aangeraden].",
        "config-db-type": "Databasetype:",
        "config-db-host": "Databasehost:",
-       "config-db-host-help": "Als uw databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.\n\nAls u gebruik maakt van gedeelde webhosting, hoort uw provider u de juiste hostnaam te hebben verstrekt.\n\nAls u MediaWiki op een Windowsserver installeert en MySQL gebruikt, dan werkt \"localhost\" mogelijk niet als servernaam.\nAls het inderdaad niet werkt, probeer dan \"127.0.0.1\" te gebruiken als lokaal IP-adres.\n\nAls u PostgreSQL gebruikt, laat dit veld dan leeg om via een Unix-socket te verbinden.",
+       "config-db-host-help": "Als uw databaseserver een andere server is, voer dan de hostnaam of het IP-adres hier in.\n\nAls u gebruik maakt van gedeelde webhosting, hoort uw provider u de juiste hostnaam te hebben verstrekt.\n\nAls u MySQL gebruikt, dan werkt \"localhost\" mogelijk niet als servernaam.\nAls het inderdaad niet werkt, probeer dan \"127.0.0.1\" te gebruiken als lokaal IP-adres.\n\nAls u PostgreSQL gebruikt, laat dit veld dan leeg om via een Unix-socket te verbinden.",
        "config-db-host-oracle": "Database-TNS:",
        "config-db-host-oracle-help": "Voer een geldige [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name] in; een tnsnames.ora-bestand moet zichtbaar zijn voor deze installatie.<br />Als u gebruik maakt van clientlibraries 10g of een latere versie, kunt u ook gebruik maken van de naamgevingsmethode [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].",
        "config-db-wiki-settings": "Identificeer deze wiki",
        "config-invalid-db-server-oracle": "Ongeldige database-TNS \"$1\".\nGebruik \"TNS Names\" of een \"Easy Connect\" tekst ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle naamgevingsmethoden])",
        "config-invalid-db-name": "Ongeldige databasenaam \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).",
        "config-invalid-db-prefix": "Ongeldig databasevoorvoegsel \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_) en streepjes (-).",
-       "config-connection-error": "$1.\n\nControleer de host, gebruikersnaam en wachtwoord en probeer het opnieuw.",
+       "config-connection-error": "$1.\n\nControleer de host, gebruikersnaam en wachtwoord en probeer het opnieuw. Probeer \"127.0.0.1\" in plaats van \"localhost\" als database host. (of omgekeerd)",
        "config-invalid-schema": "Ongeldig schema voor MediaWiki \"$1\".\nGebruik alleen letters (a-z, A-Z), cijfers (0-9) en liggende streepjes (_).",
        "config-db-sys-create-oracle": "Het installatieprogramma biedt alleen de mogelijkheid een nieuw account aan te maken met een SYSDBA-account.",
        "config-db-sys-user-exists-oracle": "Gebruikersaccount \"$1\" bestaat al. SYSDBA kan alleen gebruikt worden voor het aanmaken van een nieuw account!",
index 0bc85a1..de6d731 100644 (file)
@@ -26,7 +26,8 @@
                        "Mailman",
                        "Facenapalm",
                        "Movses",
-                       "Vlad5250"
+                       "Vlad5250",
+                       "Athena Atterdag"
                ]
        },
        "config-desc": "Инсталлятор MediaWiki",
        "config-using-32bit": "<strong>Внимание:</strong> похоже, ваша система работает с 32-битными целыми числами. Это [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:32-bit не рекомендуется].",
        "config-db-type": "Тип базы данных:",
        "config-db-host": "Хост базы данных:",
-       "config-db-host-help": "Если сервер базы данных находится на другом сервере, введите здесь его имя хоста или IP-адрес.\n\nЕсли вы используете виртуальный хостинг, ваш провайдер должен указать правильное имя хоста в своей документации.\n\nЕсли вы устанавливаете систему на сервере под Windows и используете MySQL, имя сервера «localhost» может не работать. В этом случае попробуйте указать 127.0.0.1 локальный  IP-адрес.\n\nЕсли вы используете PostgreSQL, оставьте это поле пустым для подключения через сокет Unix.",
+       "config-db-host-help": "Если ваш сервер базы данных находится на другом сервере, введите здесь его имя хоста или IP-адрес.\n\nЕсли вы используете совместный виртуальный хостинг, ваш провайдер хостинга должен сообщить вам правильное имя хоста в своей документации.\n\nЕсли вы используете MySQL, «localhost» может не подойти в качестве имени сервера. В этом случае попробуйте указать 127.0.0.1 в качестве локального IP-адреса.\n\nЕсли вы используете PostgreSQL, оставьте это поле пустым для подключения через сокет Unix.",
        "config-db-host-oracle": "TNS базы данных:",
        "config-db-host-oracle-help": "Введите действительный [http://download.oracle.com/docs/cd/B28359_01/network.111/b28317/tnsnames.htm Local Connect Name]; файл tnsnames.ora должен быть видимым для этой инсталляции. <br />При использовании клиентских библиотек версии 10g и старше также возможно использовать метод именования [http://download.oracle.com/docs/cd/E11882_01/network.112/e10836/naming.htm Easy Connect].",
        "config-db-wiki-settings": "Идентификация этой вики",
        "config-invalid-db-server-oracle": "Неверное TNS базы данных «$1».\nИспользуйте либо «TNS Name», либо строку «Easy Connect» ([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Методы наименования Oracle])",
        "config-invalid-db-name": "Неверное имя базы данных «$1».\nИспользуйте только ASCII-символы (a-z, A-Z), цифры (0-9), знак подчёркивания (_) и дефис(-).",
        "config-invalid-db-prefix": "Неверный префикс базы данных «$1».\nИспользуйте только буквы ASCII (a-z, A-Z), цифры (0-9), знак подчёркивания (_) и дефис (-).",
-       "config-connection-error": "$1.\n\nПроверьте хост, имя пользователя и пароль и попробуйте ещё раз.",
+       "config-connection-error": "$1.\n\nПроверьте хост, имя пользователя и пароль и попробуйте ещё раз. Если в качестве хоста базы данных используется \"localhost\", попробуйте использовать вместо него \"127.0.0.1\" (или наоборот).",
        "config-invalid-schema": "Неправильная схема для MediaWiki «$1».\nИспользуйте только ASCII символы (a-z, A-Z), цифры(0-9) и знаки подчёркивания(_).",
        "config-db-sys-create-oracle": "Программа установки поддерживает только использование SYSDBA для создания новой учётной записи.",
        "config-db-sys-user-exists-oracle": "Учётная запись «$1». SYSDBA может использоваться только для создания новой учётной записи!",
index e897a81..3415608 100644 (file)
@@ -22,8 +22,8 @@
        "config-no-session": "Vaši podaci o sesiji su izgubljeni!\nProverite datoteku php.ini i uverite se da je parametar <code>session.save_path</code> postavljen na odgovarajući direktorijum.",
        "config-your-language": "Vaš jezik:",
        "config-your-language-help": "Izaberite jezik koji želite da koristite tokom procesa instalacije.",
-       "config-wiki-language": "Wiki jezik:",
-       "config-wiki-language-help": "Izaberite jezik na kom će wiki biti pretežno napisan.",
+       "config-wiki-language": "Viki jezik:",
+       "config-wiki-language-help": "Izaberite jezik na kom će viki biti pretežno napisan.",
        "config-back": "← Nazad",
        "config-continue": "Nastavi →",
        "config-page-language": "Jezik",
        "config-page-releasenotes": "Napomene o izdanju",
        "config-page-copying": "Kopiranje",
        "config-page-upgradedoc": "Nadogradnja",
-       "config-page-existingwiki": "Postojeći wiki",
+       "config-page-existingwiki": "Postojeći viki",
        "config-help-restart": "Želite li da obrišete sve sačuvane podatke koje ste uneli i ponovo pokrenete proces instalacije?",
        "config-restart": "Da, pokreni ponovo",
-       "config-welcome": "=== Provera okruženja ===\nSada će se izvršiti osnovna provera kako bi se utvrdilo da li je ovo okruženje pogodno za MediaWiki instalaciju.\nNe zaboravite da uključite ove informacije ako tražite podršku kako završiti instalaciju.",
-       "config-sidebar": "* [https://www.mediawiki.org MediaWiki početna]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Vodič za korisnike]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Vodič za administratore]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ČPP]\n----\n* <doclink href=Readme>Pročitaj me</doclink>\n* <doclink href=ReleaseNotes>Napomene o izdanju</doclink>\n* <doclink href=Copying>Kopiranje</doclink>\n* <doclink href=UpgradeDoc>Nadogradnja</doclink>",
+       "config-welcome": "=== Provera okruženja ===\nSada će se izvršiti osnovna provera kako bi se utvrdilo da li je ovo okruženje pogodno za instalaciju MediaWiki-ja.\nNe zaboravite da uključite ove informacije ako tražite podršku kako završiti instalaciju.",
+       "config-sidebar": "* [https://www.mediawiki.org Početna strana MediaWiki-ja]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Vodič za korisnike]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Vodič za administratore]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ ČPP]\n----\n* <doclink href=Readme>Pročitaj me</doclink>\n* <doclink href=ReleaseNotes>Napomene o izdanju</doclink>\n* <doclink href=Copying>Kopiranje</doclink>\n* <doclink href=UpgradeDoc>Nadogradnja</doclink>",
        "config-env-good": "Okruženje je provereno.\nMožete da instalirate MediaWiki.",
        "config-env-bad": "Okruženje je provereno.\nNe možete da instalirate MediaWiki.",
        "config-env-php": "PHP $1 je instaliran.",
@@ -76,9 +76,9 @@
        "config-db-type": "Tip baze podataka:",
        "config-db-host": "Host baze podataka",
        "config-db-host-oracle": "TNS baze podataka:",
-       "config-db-wiki-settings": "Identifikuj ovaj wiki",
+       "config-db-wiki-settings": "Identifikuj ovaj viki",
        "config-db-name": "Ime baze podataka (bez crtica):",
-       "config-db-name-help": "Odaberite ime koje identifikuje vaš wiki.\nOno ne treba da sadrži razmake.\n\nAko koristite deljeni veb-hosting, vaš dobavljač usluge hostinga će vam dati određeno ime baze podataka za korišćenje ili će vas pustiti da pravite baze podataka putem kontrolne table.",
+       "config-db-name-help": "Odaberite ime koje identifikuje vaš viki.\nOno ne treba da sadrži razmake.\n\nAko koristite deljeni veb-hosting, vaš dobavljač usluge hostinga će vam dati određeno ime baze podataka za korišćenje ili će vas pustiti da pravite baze podataka putem kontrolne table.",
        "config-db-name-oracle": "Šema baze podataka:",
        "config-db-install-account": "Korisnički nalog za instalaciju",
        "config-db-username": "Korisničko ime baze podataka:",
        "config-header-sqlite": "Podešavanja SQLite-a",
        "config-header-oracle": "Podešavanja Oracle-a",
        "config-header-mssql": "Podešavanja Microsoft SQL Server-a",
-       "config-invalid-db-type": "Nevažeći tip baze podataka.",
+       "config-invalid-db-type": "Tip baze podataka nije važeći.",
        "config-missing-db-name": "Morate da unesete vrednost za „{{int:config-db-name}}”.",
        "config-missing-db-host": "Morate da unesete vrednost za „{{int:config-db-host}}”.",
        "config-missing-db-server-oracle": "Morate da unesete vrednost za „{{int:config-db-host-oracle}}”.",
-       "config-invalid-db-server-oracle": "Nevažeća TNS baza podataka „$1”.\nKoristite ili „TNS Name” ili nisku „Easy Connect”.\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle metodi imenovanja]).",
-       "config-invalid-db-name": "Nevažeće ime baze podataka „$1”.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).",
-       "config-invalid-db-prefix": "Nevažeći prefiks baze podataka „$1”.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9), podvlake (_) i crtice (-).",
+       "config-invalid-db-server-oracle": "TNS baza podataka „$1” nije važeća.\nKoristite ili „TNS Name” ili nisku „Easy Connect”.\n([http://docs.oracle.com/cd/E11882_01/network.112/e10836/naming.htm Oracle metodi imenovanja]).",
+       "config-invalid-db-name": "Ime baze podataka „$1” nije važeće.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).",
+       "config-invalid-db-prefix": "Prefiks baze podataka „$1” nije važeći.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9), podvlake (_) i crtice (-).",
        "config-connection-error": "$1.\n\nProverite host, korisničko ime i lozinku, pa pokušajte ponovo.",
-       "config-invalid-schema": "Nevažeća šema za MediaWiki „$1”.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).",
+       "config-invalid-schema": "Šema za MediaWiki „$1” nije važeća.\nKoristite samo ASCII slova (a-z, A-Z), brojeve (0-9) i podvlake (_).",
        "config-db-sys-create-oracle": "Instalacioni program podržava samo korišćenje SYSDBA naloga za otvaranje novog.",
        "config-db-sys-user-exists-oracle": "Korisnički nalog „$1” već postoji. SYSDBA se samo može koristiti za otvaranje novog naloga!",
        "config-postgres-old": "Neophodan je PostgreSQL $1 ili noviji. Vi imate $2.",
        "config-mssql-old": "Neophodan je Microsoft SQL Server $1 ili noviji. Vi imate $2.",
-       "config-sqlite-name-help": "Odaberite ime koje identifikuje vaš wiki.\nNe koristite razmake ili crtice.\nOvo će se koristiti za ime datoteke SQLite podataka.",
+       "config-sqlite-name-help": "Odaberite ime koje identifikuje vaš viki.\nNe koristite razmake ili crtice.\nOvo će se koristiti za ime datoteke SQLite podataka.",
        "config-sqlite-mkdir-error": "Greška pri pravljenju direktorijuma sa podacima „$1”.\nProverite lokaciju, pa pokušajte ponovo.",
        "config-sqlite-dir-unwritable": "Nije moguće upisati u direktorijum „$1”.\nPromenite mu dozvole, tako da veb-server može da upisuje u njemu, pa pokušajte ponovo.",
        "config-sqlite-connection-error": "$1.\n\nProverite direktorijum sa podacima i ime baze podataka ispod, pa pokušajte ponovo.",
        "config-sqlite-fts3-downgrade": "PHP-u nedostaje FTS3 podrška, poništavanje nadogradnje tabela.",
        "config-can-upgrade": "Postoje MediaWiki tabele u ovoj bazi podataka.\nDa biste ih nadogradili na MediaWiki $1, kliknite na <strong>Nastavi</strong>.",
        "config-upgrade-error": "Došlo je do greške pri nadogradnji MediaWiki tabela u bazi podataka.\n\nZa više informacija, pogledajte evidenciju iznad, da biste pokušali ponovo, kliknite na <strong>Nastavi</strong>.",
-       "config-upgrade-done": "Nadogradnja je završena.\n\nSada možete da [$1 počnete da koristite svoj wiki].\n\nAko želite da ponovo generišete datoteku <code>LocalSettings.php</code>, kliknite na dugme ispod.\nOvo se <strong>ne preporučuje</strong> osim ako imate probleme sa wiki-jem.",
-       "config-upgrade-done-no-regenerate": "Nadogradnja je završena.\n\nSada možete da [$1 počnete da koristite svoj wiki].",
+       "config-upgrade-done": "Nadogradnja je završena.\n\nSada možete da [$1 počnete da koristite svoj viki].\n\nAko želite da ponovo generišete datoteku <code>LocalSettings.php</code>, kliknite na dugme ispod.\nOvo se <strong>ne preporučuje</strong> osim ako imate probleme sa vikijem.",
+       "config-upgrade-done-no-regenerate": "Nadogradnja je završena.\n\nSada možete da [$1 počnete da koristite svoj viki].",
        "config-regenerate": "Regeneriši LocalSettings.php →",
        "config-show-table-status": "Upit <code>SHOW TABLE STATUS</code> nije uspeo!",
        "config-unknown-collation": "<strong>Upozorenje:</strong> Baza podataka koristi neprepoznata pravila poređenja.",
        "config-db-web-account": "Nalog baze podataka za veb-pristup",
-       "config-db-web-help": "Izaberite korisnički ime i lozinku koju će veb-server koristiti za povezivanje sa serverom baze podataka, tokom svakodnevnog rada na wiki-ju.",
+       "config-db-web-help": "Izaberite korisnički ime i lozinku koju će veb-server koristiti za povezivanje sa serverom baze podataka, tokom svakodnevnog rada na vikiju.",
        "config-db-web-account-same": "Koristi isti nalog kao i za instalaciju",
        "config-db-web-create": "Otvori nalog ako već ne postoji.",
        "config-db-web-no-create-privs": "Nalog koji ste naveli za instalaciju nema dovoljne privilegije da otvori nalog.\nNalog koji ovde navedete već mora da postoji.",
        "config-mssql-auth": "Tip potvrde identiteta:",
        "config-mssql-sqlauth": "SQL Server potvrda identiteta",
        "config-mssql-windowsauth": "Windows potvrda identiteta",
-       "config-site-name": "Ime wiki-ja:",
+       "config-site-name": "Ime vikija:",
        "config-site-name-help": "Ovo će se pojaviti u naslovnoj traci pregledača i na raznim drugim mestima.",
-       "config-site-name-blank": "Unesite ime sajta.",
+       "config-site-name-blank": "Unesite ime lokacije.",
        "config-project-namespace": "Imenski prostor projekta:",
        "config-ns-generic": "Projekat",
-       "config-ns-site-name": "Isti kao wiki ime: $1",
+       "config-ns-site-name": "Isti kao viki ime: $1",
        "config-ns-other": "Drugo (navedite)",
        "config-ns-other-default": "MyWiki",
        "config-ns-invalid": "Navedeni imenski prostor „<nowiki>$1</nowiki>” nije važeći.\nNavedite drugi imenski prostor projekta.",
        "config-admin-name": "Vaše korisničko ime:",
        "config-admin-password": "Lozinka:",
        "config-admin-password-confirm": "Ponovite lozinku:",
-       "config-admin-help": "Ovde unesite željeno korisničko ime; na primer, „Aleksandar Živković”.\nOvo ime ćete koristiti za prijavu na wiki.",
+       "config-admin-help": "Ovde unesite željeno korisničko ime; na primer, „Aleksandar Živković”.\nOvo ime ćete koristiti za prijavu na viki.",
        "config-admin-name-blank": "Unesite korisničko ime administratora.",
        "config-admin-name-invalid": "Navedeno korisničko ime „<nowiki>$1</nowiki>” nije važeće.\nNavedite drugo.",
        "config-admin-password-blank": "Unesite lozinku za nalog administratora.",
        "config-subscribe": "Pretplatite se na [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce release announcements dopisnu listu].",
        "config-subscribe-noemail": "Pokušali ste da se pretplatite na dopisnu listu za objave o novim izdanjima bez pružanja adrese e-pošte.\nPružite adresu e-pošte ako želite da se pretplatite na nju.",
        "config-pingback": "Deli podatke o ovoj instalaciji sa MediaWiki programerima.",
-       "config-almost-done": "Skoro ste gotovi!\nSada možete preskočiti preostalu konfiguraciju i odmah instalirati wiki.",
+       "config-almost-done": "Skoro ste gotovi!\nSada možete preskočiti preostalu konfiguraciju i odmah instalirati viki.",
        "config-optional-continue": "Postavi mi još pitanja.",
-       "config-optional-skip": "Već mi je dosadno, samo instaliraj wiki.",
+       "config-optional-skip": "Već mi je dosadno, samo instaliraj viki.",
        "config-profile": "Profil korisničkih grupa:",
-       "config-profile-wiki": "Otvoren wiki",
+       "config-profile-wiki": "Otvoren viki",
        "config-profile-no-anon": "Neophodno je otvoriti nalog",
        "config-profile-fishbowl": "Samo ovlašćeni urednici",
-       "config-profile-private": "Privatan wiki",
-       "config-profile-help": "Wiki-ji najbolje funkcionišu kada dozvoljavate što više korisnika da uređuju kako je to moguće.\nU MediaWiki-ju, lako je pregledati nedavne promene i vratiti svaku štetu koju počine naivni ili zlonamerni korisnici.\n\nMeđutim, mnogi su pronašli MediaWiki da je koristan u širokoj raznolikosti uloga, a ponekad nije lako uveriti se u sve prednosti načina wiki-ja.\nTako da imate izbor.\n\nModel <strong>{{int:config-profile-wiki}}</strong> dozvoljava svima da uređuju, bez prijavljivanja.\nWiki-ji sa <strong>{{int:config-profile-no-anon}}</strong> pružaju dodatnu odgovornost, ali može sprečiti slučajne doprinose.\n\n<strong>{{int:config-profile-fishbowl}}</strong> scenario dozvoljava odobrenim korisnicima da uređuju, ali svi mogu videti stranice, uključujući istoriju.\n<strong>{{int:config-profile-private}}</strong> samo dozvoljava odobrenim korisnicima da vide stranice, sa istom grupom dozvoljenom da uređuje.\n\nSložene konfiguracije korisničkih prava su dostupne nakon instalacije, pogledajte [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights odgovarajući ručni unos].",
+       "config-profile-private": "Privatan viki",
+       "config-profile-help": "Vikiji najbolje funkcionišu kada dozvoljavate što više korisnika da uređuju kako je to moguće.\nU MediaWiki-ju, lako je pregledati nedavne promene i vratiti svaku štetu koju počine naivni ili zlonamerni korisnici.\n\nMeđutim, mnogi su pronašli MediaWiki da je koristan u širokoj raznolikosti uloga, a ponekad nije lako uveriti se u sve prednosti načina vikija.\nTako da imate izbor.\n\nModel <strong>{{int:config-profile-wiki}}</strong> dozvoljava svima da uređuju, bez prijavljivanja.\nVikiji sa <strong>{{int:config-profile-no-anon}}</strong> pružaju dodatnu odgovornost, ali može sprečiti slučajne doprinose.\n\n<strong>{{int:config-profile-fishbowl}}</strong> scenario dozvoljava odobrenim korisnicima da uređuju, ali svi mogu videti stranice, uključujući istoriju.\n<strong>{{int:config-profile-private}}</strong> samo dozvoljava odobrenim korisnicima da vide stranice, sa istom grupom dozvoljenom da uređuje.\n\nSložene konfiguracije korisničkih prava su dostupne nakon instalacije, pogledajte [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:User_rights odgovarajući ručni unos].",
        "config-license": "Autorska prava i licenca:",
        "config-license-none": "Bez podnožja za licencu",
        "config-license-cc-by-sa": "Creative Commons Autorstvo-Deliti pod istim uslovima",
        "config-cc-not-chosen": "Odaberite koju Creative Commons licencu želite i kliknite na „nastavi”.",
        "config-advanced-settings": "Napredna konfiguracija",
        "config-cache-options": "Podešavanja za keširanje objekta:",
-       "config-cache-none": "Nema keširanja (funkcionalnost nije uklonjena, ali brzina može uticati na veće wiki sajtove)",
+       "config-cache-none": "Nema keširanja (funkcionalnost nije uklonjena, ali brzina može uticati na veće viki lokacije)",
        "config-cache-accel": "Keširanje PHP objekta (APC, APCu or WinCache)",
        "config-cache-memcached": "Koristi Memcached (zahteva dodatno podešavanje i konfiguraciju)",
        "config-memcached-servers": "Memcached serveri:",
        "config-install-mainpage-exists": "Glavna strana već postoji, preskakanje",
        "config-install-extension-tables": "Pravljenje tabela za omogućene dodatke",
        "config-install-mainpage-failed": "Nije moguće umetnuti glavnu stranu: $1",
-       "config-install-done": "<strong>Čestitamo!</strong>\nInstalirali ste MediaWiki.\n\nInstalacioni program je generisao datoteku <code>LocalSettings.php</code>.\nOna sadrži svu vašu konfiguraciju.\n\nMoraćete da je preuzmete i stavite u bazu vaše wiki instalacije (isti direktorijum kao index.php). Preuzimanje bi automatski trebalo početi.\n\nAko preuzimanje nije ponuđeno, ili ako ga otkažete, možete ponovo pokrenuti preuzimanje tako što ćete kliknuti na dolenavedenu vezu:\n\n$3\n\n<strong>Napomena:</strong> Ako to odmah ne uradite, ova generisana konfiguraciona datoteka neće vam biti dostupna kasnije ako izađete iz instalacije bez preuzimanja.\n\nKada je to učinjeno, možete da <strong>[$2 posetite svoj wiki]</strong>.",
-       "config-install-done-path": "<strong>Čestitamo!</strong>\nInstalirali ste MediaWiki.\n\nInstalacioni program je generisao datoteku <code>LocalSettings.php</code>.\nOna sadrži svu vašu konfiguraciju.\n\nMoraćete da je preuzmete i stavite u <code>$4</code>. Preuzimanje bi automatski trebalo početi.\n\nAko preuzimanje nije ponuđeno, ili ako ga otkažete, možete ponovo pokrenuti preuzimanje tako što ćete kliknuti na dolenavedenu vezu:\n\n$3\n\n<strong>Napomena:</strong> Ako to odmah ne uradite, ova generisana konfiguraciona datoteka neće vam biti dostupna kasnije ako izađete iz instalacije bez preuzimanja.\n\nKada je to učinjeno, možete da <strong>[$2 posetite svoj wiki]</strong>.",
-       "config-install-success": "MediaWiki je uspešno instaliran. Sada možete posetiti <$1$2> da biste videli svoj wiki.\nAko imate pitanja, pogledajte našu listu često postavljanih pitanja: <https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ> ili koristite jedan od foruma za podršku koji su povezani na toj stranici.",
+       "config-install-done": "<strong>Čestitamo!</strong>\nInstalirali ste MediaWiki.\n\nInstalacioni program je generisao datoteku <code>LocalSettings.php</code>.\nOna sadrži svu vašu konfiguraciju.\n\nMoraćete da je preuzmete i stavite u bazu vaše viki instalacije (isti direktorijum kao index.php). Preuzimanje bi automatski trebalo početi.\n\nAko preuzimanje nije ponuđeno, ili ako ga otkažete, možete ponovo pokrenuti preuzimanje tako što ćete kliknuti na dolenavedenu vezu:\n\n$3\n\n<strong>Napomena:</strong> Ako to odmah ne uradite, ova generisana konfiguraciona datoteka neće vam biti dostupna kasnije ako izađete iz instalacije bez preuzimanja.\n\nKada je to učinjeno, možete da <strong>[$2 posetite svoj viki]</strong>.",
+       "config-install-done-path": "<strong>Čestitamo!</strong>\nInstalirali ste MediaWiki.\n\nInstalacioni program je generisao datoteku <code>LocalSettings.php</code>.\nOna sadrži svu vašu konfiguraciju.\n\nMoraćete da je preuzmete i stavite u <code>$4</code>. Preuzimanje bi automatski trebalo početi.\n\nAko preuzimanje nije ponuđeno, ili ako ga otkažete, možete ponovo pokrenuti preuzimanje tako što ćete kliknuti na dolenavedenu vezu:\n\n$3\n\n<strong>Napomena:</strong> Ako to odmah ne uradite, ova generisana konfiguraciona datoteka neće vam biti dostupna kasnije ako izađete iz instalacije bez preuzimanja.\n\nKada je to učinjeno, možete da <strong>[$2 posetite svoj viki]</strong>.",
+       "config-install-success": "MediaWiki je uspešno instaliran. Sada možete posetiti <$1$2> da biste videli svoj viki.\nAko imate pitanja, pogledajte našu listu često postavljanih pitanja: <https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ> ili koristite jedan od foruma za podršku koji su povezani na toj stranici.",
        "config-download-localsettings": "Preuzmi datoteku <code>LocalSettings.php</code>",
        "config-help": "pomoć",
        "config-help-tooltip": "kliknite da biste proširili",
        "config-nofile": "Nije moguće pronaći datoteku „$1”. Nije li izbrisana?",
-       "config-extension-link": "Jeste li znali da vaš wiki podržava [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions dodatke]?\n\nMožete ih pregledati [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category po kategoriji].",
+       "config-extension-link": "Jeste li znali da vaš viki podržava [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions dodatke]?\n\nMožete ih pregledati [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category po kategoriji].",
        "config-skins-screenshots": "„$1” (snimci ekrana: $2)",
        "config-skins-screenshot": "$1 ($2)",
        "config-extensions-requires": "$1 (zahteva $2)",
        "config-extension-not-found": "Nije moguće pronaći datoteku registracije za dodatak „$1”",
        "config-extension-dependency": "Došlo je do greške zavisnosti pri instaliranju dodatka „$1”: $2",
        "mainpagetext": "<strong>MediaWiki je instaliran.</strong>",
-       "mainpagedocfooter": "Pogledajte [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Vodič za korisnike] za informacije o tome kako koristiti wiki softver.\n\n== Uvod ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista postavki konfiguracije]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki ČPP]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Dopisna lista o MediaWiki izdanjima]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Naučite kako da se borite protiv nepoželjnog sadržaja na svom wiki-ju]"
+       "mainpagedocfooter": "Pogledajte [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Vodič za korisnike] za informacije o tome kako koristiti viki softver.\n\n== Uvod ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Lista postavki konfiguracije]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki ČPP]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce Dopisna lista o MediaWiki izdanjima]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Naučite kako da se borite protiv nepoželjnog sadržaja na svom vikiju]"
 }
index 1594e94..84b5b03 100644 (file)
@@ -31,6 +31,7 @@
        "config-page-existingwiki": "ಇತ್ತೆದ ವಿಕಿ",
        "config-restart": "ಸರಿ,ಕುಡ ಸುರು ಮಲ್ಪುಲೆ",
        "config-db-type": "ದತ್ತಾಂಶಸಂಚಯ ಮಾದರಿ:",
+       "config-db-host-help": "ಇರೆನ ದತ್ತಸಂಚಯ ಸೇವಕ ಬೇತೆ ಸೇವಕೊ(ಸರ್ವರ್)ಡು ಇತ್ತ್ಂಡ, ಆಶ್ರಯದಾತ ಪುದರು ಇಜಿಂಡ ಐಪಿ ವಿಳಾಸ ಮುಲ್ಪ ಸೇರಾಲೆ.\nಈರ್ ಪಾಲುದ ಜಾಲ ಆಶ್ರಯ ಬಳಸುನಾಂಡಾ, ಇರೆನ ಆಶ್ರಯ ದಾತೆರ್ ಅಕಲೆನ ದಾಖಲಿಕೆಡ್ ಇರೆಗ್ ಸರಿಯಾಯಿನ ಆಶ್ರಯದಾತ ನಾಮ ಕೊರೊಡು.\nಈರ್ MySQL ಬಳಸುನಾಂಡ,\"localhost\" (\"ತಲ-ಆಶ್ರಯದಾತ\")ಬಳಕೆ ಆಶ್ರಯದಾತ ಪುದರುಗು ಬೇಲೆ ಮಲ್ಪಂದ್.ಅವು ಆಯಿಜಿಡ, ತಲ ಐಪಿ ವಿಳಾಸೊಗು \"127.0.0.1\" ಪಾಡ್ದ್ ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ.\nಈರ್ PostgreSQL ಬಳಸುನಾಂಡ, ಈ ಕ್ಷೇತ್ರೊನು ಖಾಲಿ ಬುಡುದು,ಯುನಿಕ್ಸ್ ಗುರಿತ ಮೂಲಕ ಕೂಡಾಲೆ.",
        "config-db-host-oracle": "ದತ್ತಾಂಶಸಂಚಯ TNS:",
        "config-db-wiki-settings": "ಈ ವಿಕಿಯನ್ನು ಗುರುತಿಸಾಲೆ",
        "config-db-name": "ಮಾಹಿತಿಕೋಶದ ಪುದರ್(ಕೂಡುಗೆರೆ ದಾಂತೆ):",
index f6265f7..7a8e64f 100644 (file)
@@ -214,7 +214,7 @@ class ClassicInterwikiLookup implements InterwikiLookup {
        private function getInterwikiCacheEntry( $prefix ) {
                wfDebug( __METHOD__ . "( $prefix )\n" );
 
-               $wikiId = WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() );
+               $wikiId = WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() );
 
                $value = false;
                try {
@@ -339,7 +339,7 @@ class ClassicInterwikiLookup implements InterwikiLookup {
        private function getAllPrefixesCached( $local ) {
                wfDebug( __METHOD__ . "()\n" );
 
-               $wikiId = WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() );
+               $wikiId = WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() );
 
                $data = [];
                try {
diff --git a/includes/jobqueue/IJobSpecification.php b/includes/jobqueue/IJobSpecification.php
new file mode 100644 (file)
index 0000000..8bc1bc3
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Job queue task description base code.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Job queue task description interface
+ *
+ * @ingroup JobQueue
+ * @since 1.23
+ */
+interface IJobSpecification {
+       /**
+        * @return string Job type
+        */
+       public function getType();
+
+       /**
+        * @return array
+        */
+       public function getParams();
+
+       /**
+        * @return int|null UNIX timestamp to delay running this job until, otherwise null
+        */
+       public function getReleaseTimestamp();
+
+       /**
+        * @return bool Whether only one of each identical set of jobs should be run
+        */
+       public function ignoreDuplicates();
+
+       /**
+        * Subclasses may need to override this to make duplication detection work.
+        * The resulting map conveys everything that makes the job unique. This is
+        * only checked if ignoreDuplicates() returns true, meaning that duplicate
+        * jobs are supposed to be ignored.
+        *
+        * @return array Map of key/values
+        */
+       public function getDeduplicationInfo();
+
+       /**
+        * @see JobQueue::deduplicateRootJob()
+        * @return array
+        * @since 1.26
+        */
+       public function getRootJobParams();
+
+       /**
+        * @see JobQueue::deduplicateRootJob()
+        * @return bool
+        * @since 1.22
+        */
+       public function hasRootJobParams();
+
+       /**
+        * @see JobQueue::deduplicateRootJob()
+        * @return bool Whether this is job is a root job
+        */
+       public function isRootJob();
+
+       /**
+        * @return Title Descriptive title (this can simply be informative)
+        */
+       public function getTitle();
+}
index 4f4728d..660352a 100644 (file)
@@ -366,7 +366,7 @@ abstract class JobQueue {
                global $wgJobClasses;
 
                $this->assertNotReadOnly();
-               if ( !WikiMap::isCurrentWikiDomain( $this->domain ) ) {
+               if ( !WikiMap::isCurrentWikiDbDomain( $this->domain ) ) {
                        throw new MWException(
                                "Cannot pop '{$this->type}' job off foreign '{$this->domain}' wiki queue." );
                } elseif ( !isset( $wgJobClasses[$this->type] ) ) {
@@ -724,17 +724,3 @@ abstract class JobQueue {
                $stats->updateCount( "jobqueue.{$key}.{$type}", $delta );
        }
 }
-
-/**
- * @ingroup JobQueue
- * @since 1.22
- */
-class JobQueueError extends MWException {
-}
-
-class JobQueueConnectionError extends JobQueueError {
-}
-
-class JobQueueReadOnlyError extends JobQueueError {
-
-}
index 26886b7..2025bf7 100644 (file)
@@ -180,7 +180,6 @@ class JobQueueFederated extends JobQueue {
                // Try to insert the jobs and update $partitionsTry on any failures.
                // Retry to insert any remaning jobs again, ignoring the bad partitions.
                $jobsLeft = $jobs;
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $i = $this->maxPartitionsTry; $i > 0 && count( $jobsLeft ); --$i ) {
                        try {
                                $partitionRing->getLiveLocationWeights();
index 4853c4a..328f298 100644 (file)
@@ -71,16 +71,16 @@ class JobQueueGroup {
                global $wgLocalDatabases;
 
                if ( $domain === false ) {
-                       $domain = WikiMap::getCurrentWikiDomain()->getId();
+                       $domain = WikiMap::getCurrentWikiDbDomain()->getId();
                }
 
                if ( !isset( self::$instances[$domain] ) ) {
                        self::$instances[$domain] = new self( $domain, wfConfiguredReadOnlyReason() );
                        // Make sure jobs are not getting pushed to bogus wikis. This can confuse
                        // the job runner system into spawning endless RPC requests that fail (T171371).
-                       $wikiId = WikiMap::getWikiIdFromDomain( $domain );
+                       $wikiId = WikiMap::getWikiIdFromDbDomain( $domain );
                        if (
-                               !WikiMap::isCurrentWikiDomain( $domain ) &&
+                               !WikiMap::isCurrentWikiDbDomain( $domain ) &&
                                !in_array( $wikiId, $wgLocalDatabases )
                        ) {
                                self::$instances[$domain]->invalidDomain = true;
@@ -430,10 +430,10 @@ class JobQueueGroup {
         */
        private function getCachedConfigVar( $name ) {
                // @TODO: cleanup this whole method with a proper config system
-               if ( WikiMap::isCurrentWikiDomain( $this->domain ) ) {
+               if ( WikiMap::isCurrentWikiDbDomain( $this->domain ) ) {
                        return $GLOBALS[$name]; // common case
                } else {
-                       $wiki = WikiMap::getWikiIdFromDomain( $this->domain );
+                       $wiki = WikiMap::getWikiIdFromDbDomain( $this->domain );
                        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                        $value = $cache->getWithSetCallback(
                                $cache->makeGlobalKey( 'jobqueue', 'configvalue', $this->domain, $name ),
index a1ef28b..5e7a115 100644 (file)
@@ -811,7 +811,7 @@ LUA;
                $type = is_string( $type ) ? $type : $this->type;
 
                // Use wiki ID for b/c
-               $keyspace = WikiMap::getWikiIdFromDomain( $this->domain );
+               $keyspace = WikiMap::getWikiIdFromDbDomain( $this->domain );
 
                $parts = [ $keyspace, 'jobqueue', $type, $prop ];
 
index d060c1c..4abbc6d 100644 (file)
  * @file
  */
 
-/**
- * Job queue task description interface
- *
- * @ingroup JobQueue
- * @since 1.23
- */
-interface IJobSpecification {
-       /**
-        * @return string Job type
-        */
-       public function getType();
-
-       /**
-        * @return array
-        */
-       public function getParams();
-
-       /**
-        * @return int|null UNIX timestamp to delay running this job until, otherwise null
-        */
-       public function getReleaseTimestamp();
-
-       /**
-        * @return bool Whether only one of each identical set of jobs should be run
-        */
-       public function ignoreDuplicates();
-
-       /**
-        * Subclasses may need to override this to make duplication detection work.
-        * The resulting map conveys everything that makes the job unique. This is
-        * only checked if ignoreDuplicates() returns true, meaning that duplicate
-        * jobs are supposed to be ignored.
-        *
-        * @return array Map of key/values
-        */
-       public function getDeduplicationInfo();
-
-       /**
-        * @see JobQueue::deduplicateRootJob()
-        * @return array
-        * @since 1.26
-        */
-       public function getRootJobParams();
-
-       /**
-        * @see JobQueue::deduplicateRootJob()
-        * @return bool
-        * @since 1.22
-        */
-       public function hasRootJobParams();
-
-       /**
-        * @see JobQueue::deduplicateRootJob()
-        * @return bool Whether this is job is a root job
-        */
-       public function isRootJob();
-
-       /**
-        * @return Title Descriptive title (this can simply be informative)
-        */
-       public function getTitle();
-}
-
 /**
  * Job queue task description base code
  *
diff --git a/includes/jobqueue/exception/JobQueueConnectionError.php b/includes/jobqueue/exception/JobQueueConnectionError.php
new file mode 100644 (file)
index 0000000..7f32f23
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Job queue base code.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @defgroup JobQueue JobQueue
+ */
+
+/**
+ * @ingroup JobQueue
+ * @since 1.22
+ */
+class JobQueueConnectionError extends JobQueueError {
+}
diff --git a/includes/jobqueue/exception/JobQueueError.php b/includes/jobqueue/exception/JobQueueError.php
new file mode 100644 (file)
index 0000000..19cb555
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Job queue base code.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @defgroup JobQueue JobQueue
+ */
+
+/**
+ * @ingroup JobQueue
+ * @since 1.22
+ */
+class JobQueueError extends MWException {
+}
diff --git a/includes/jobqueue/exception/JobQueueReadOnlyError.php b/includes/jobqueue/exception/JobQueueReadOnlyError.php
new file mode 100644 (file)
index 0000000..b5c20ce
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Job queue base code.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @defgroup JobQueue JobQueue
+ */
+
+/**
+ * @ingroup JobQueue
+ * @since 1.22
+ */
+class JobQueueReadOnlyError extends JobQueueError {
+
+}
index c2c3b47..f3c1f01 100644 (file)
@@ -793,7 +793,7 @@ class FSFileBackend extends FileBackendStore {
         * @param int $errno
         * @param string $errstr
         * @return bool
-        * @access private
+        * @private
         */
        public function handleWarning( $errno, $errstr ) {
                $this->logger->error( $errstr ); // more detailed error logging
index 51407a7..aea0a02 100644 (file)
@@ -14,9 +14,6 @@
  * specific language governing permissions and limitations under the License.
  */
 
-// strlen() is actually pretty fast compared to just about any loop body
-// phpcs:disable Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed
-
 /**
  * Read the directory of a Microsoft Compound File Binary file, a.k.a. an OLE
  * file, and detect the MIME type.
index a2075dc..e08da61 100644 (file)
@@ -1148,7 +1148,7 @@ EOT;
         * distinguish them from MIME types.
         *
         * This function relies on the mapping defined by $this->mMediaTypes
-        * @access private
+        * @private
         * @param string $extMime
         * @return int|string
         */
index 6e8d266..88f87f8 100644 (file)
@@ -1334,7 +1334,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        $this->setInterimValue( $key, $wrapped, $tempTTL );
                }
 
-               if ( $valueIsCacheable ) {
+               // Save the value unless a mutex-winning thread is already expected to do that
+               if ( $valueIsCacheable && ( !$useMutex || $lockAcquired ) ) {
                        $setOpts['lockTSE'] = $lockTSE;
                        $setOpts['staleTTL'] = $staleTTL;
                        // Use best known "since" timestamp if not provided
index 9c6c907..764230b 100644 (file)
@@ -97,4 +97,27 @@ class WinCacheBagOStuff extends BagOStuff {
 
                return $keyspace . ':' . implode( ':', $args );
        }
+
+       /**
+        * Increase stored value of $key by $value while preserving its original TTL
+        * @param string $key Key to increase
+        * @param int $value Value to add to $key (Default 1)
+        * @return int|bool New value or false on failure
+        */
+       public function incr( $key, $value = 1 ) {
+               if ( !$this->lock( $key ) ) {
+                       return false;
+               }
+               $n = $this->get( $key );
+               if ( $this->isInteger( $n ) ) { // key exists?
+                       $n += intval( $value );
+                       $oldTTL = wincache_ucache_info( false, $key )["ucache_entries"][1]["ttl_seconds"];
+                       $this->set( $key, max( 0, $n ), $oldTTL );
+               } else {
+                       $n = false;
+               }
+               $this->unlock( $key );
+
+               return $n;
+       }
 }
index 7d971af..974c7df 100644 (file)
@@ -820,7 +820,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        }
 
        public function getFlag( $flag ) {
-               return !!( $this->flags & $flag );
+               return (bool)( $this->flags & $flag );
        }
 
        /**
index 7987052..98c06ad 100644 (file)
@@ -48,6 +48,7 @@ interface ILBFactory {
         *  - wanCache: WANObjectCache object [optional]
         *  - hostname: The name of the current server [optional]
         *  - cliMode: Whether the execution context is a CLI script. [optional]
+        *  - maxLag: Try to avoid DB replicas with lag above this many seconds [optional]
         *  - profiler: Class name or instance with profileIn()/profileOut() methods. [optional]
         *  - trxProfiler: TransactionProfiler instance. [optional]
         *  - replLogger: PSR-3 logger instance. [optional]
index 9a6c224..9ec1ce1 100644 (file)
@@ -95,6 +95,9 @@ abstract class LBFactory implements ILBFactory {
        /** @var string|null */
        private $defaultGroup = null;
 
+       /** @var int|null */
+       protected $maxLag;
+
        const ROUND_CURSORY = 'cursory';
        const ROUND_BEGINNING = 'within-begin';
        const ROUND_COMMITTING = 'within-commit';
@@ -110,6 +113,7 @@ abstract class LBFactory implements ILBFactory {
                        ? DatabaseDomain::newFromId( $conf['localDomain'] )
                        : DatabaseDomain::newUnspecified();
 
+               $this->maxLag = $conf['maxLag'] ?? null;
                if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
                        $this->readOnlyReason = $conf['readOnlyReason'];
                }
@@ -588,6 +592,7 @@ abstract class LBFactory implements ILBFactory {
                        'hostname' => $this->hostname,
                        'cliMode' => $this->cliMode,
                        'agent' => $this->agent,
+                       'maxLag' => $this->maxLag,
                        'defaultGroup' => $this->defaultGroup,
                        'chronologyCallback' => function ( ILoadBalancer $lb ) {
                                // Defer ChronologyProtector construction in case setRequestInfo() ends up
index cc6824d..189ceee 100644 (file)
@@ -107,12 +107,6 @@ class LBFactoryMulti extends LBFactory {
        /** @var string */
        private $lastSection;
 
-       /** @var int */
-       private $maxLag = self::MAX_LAG_DEFAULT;
-
-       /** @var int Default 'maxLag' when unspecified */
-       const MAX_LAG_DEFAULT = 10;
-
        /**
         * @see LBFactory::__construct()
         *
@@ -166,7 +160,6 @@ class LBFactoryMulti extends LBFactory {
         *                                 storage cluster.
         *   - masterTemplateOverrides     Server configuration map overrides for all master servers.
         *   - loadMonitorClass            Name of the LoadMonitor class to always use.
-        *   - maxLag                      Avoid replica DBs with more lag than this many seconds.
         *   - readOnlyBySection           A map of section name to read-only message.
         *                                 Missing or false for read/write.
         */
@@ -178,7 +171,7 @@ class LBFactoryMulti extends LBFactory {
                $optional = [ 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
                        'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
                        'templateOverridesByCluster', 'templateOverridesBySection', 'masterTemplateOverrides',
-                       'readOnlyBySection', 'maxLag', 'loadMonitorClass' ];
+                       'readOnlyBySection', 'loadMonitorClass' ];
 
                foreach ( $required as $key ) {
                        if ( !isset( $conf[$key] ) ) {
@@ -318,7 +311,6 @@ class LBFactoryMulti extends LBFactory {
                        $this->baseLoadBalancerParams(),
                        [
                                'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
-                               'maxLag' => $this->maxLag,
                                'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
                                'readOnlyReason' => $readOnlyReason
                        ]
index 6a6bb8d..49054e0 100644 (file)
@@ -41,11 +41,6 @@ class LBFactorySimple extends LBFactory {
 
        /** @var string */
        private $loadMonitorClass;
-       /** @var int */
-       private $maxLag;
-
-       /** @var int Default 'maxLag' when unspecified */
-       const MAX_LAG_DEFAULT = 10;
 
        /**
         * @see LBFactory::__construct()
@@ -73,7 +68,6 @@ class LBFactorySimple extends LBFactory {
 
                $this->externalClusters = $conf['externalClusters'] ?? [];
                $this->loadMonitorClass = $conf['loadMonitorClass'] ?? 'LoadMonitor';
-               $this->maxLag = $conf['maxLag'] ?? self::MAX_LAG_DEFAULT;
        }
 
        /**
@@ -130,7 +124,6 @@ class LBFactorySimple extends LBFactory {
                        $this->baseLoadBalancerParams(),
                        [
                                'servers' => $servers,
-                               'maxLag' => $this->maxLag,
                                'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
                        ]
                ) );
index e1a3162..b20bf04 100644 (file)
@@ -101,7 +101,7 @@ interface ILoadBalancer {
         *  - loadMonitor : Name of a class used to fetch server lag and load.
         *  - readOnlyReason : Reason the master DB is read-only if so [optional]
         *  - waitTimeout : Maximum time to wait for replicas for consistency [optional]
-        *  - maxLag: Avoid replica DB servers with more lag than this [optional]
+        *  - maxLag: Try to avoid DB replicas with lag above this many seconds [optional]
         *  - srvCache : BagOStuff object for server cache [optional]
         *  - wanCache : WANObjectCache object [optional]
         *  - chronologyCallback: Callback to run before the first connection attempt [optional]
index ab5c3cd..b3f9eff 100644 (file)
@@ -85,7 +85,7 @@ class LoadBalancer implements ILoadBalancer {
        /** @var string Alternate ID string for the domain instead of DatabaseDomain::getId() */
        private $localDomainIdAlias;
        /** @var int */
-       private $maxLag = self::MAX_LAG_DEFAULT;
+       private $maxLag;
 
        /** @var string Current server name */
        private $hostname;
@@ -134,7 +134,7 @@ class LoadBalancer implements ILoadBalancer {
        const CONN_HELD_WARN_THRESHOLD = 10;
 
        /** @var int Default 'maxLag' when unspecified */
-       const MAX_LAG_DEFAULT = 10;
+       const MAX_LAG_DEFAULT = 6;
        /** @var int Default 'waitTimeout' when unspecified */
        const MAX_WAIT_DEFAULT = 10;
        /** @var int Seconds to cache master server read-only status */
@@ -200,9 +200,7 @@ class LoadBalancer implements ILoadBalancer {
                        $this->readOnlyReason = $params['readOnlyReason'];
                }
 
-               if ( isset( $params['maxLag'] ) ) {
-                       $this->maxLag = $params['maxLag'];
-               }
+               $this->maxLag = $params['maxLag'] ?? self::MAX_LAG_DEFAULT;
 
                $this->loadMonitorConfig = $params['loadMonitor'] ?? [ 'class' => 'LoadMonitorNull' ];
                $this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
@@ -1074,7 +1072,7 @@ class LoadBalancer implements ILoadBalancer {
         * Test if the specified index represents an open connection
         *
         * @param int $index Server index
-        * @access private
+        * @private
         * @return bool
         */
        private function isOpen( $index ) {
index 84cf0b2..ddecf9e 100644 (file)
@@ -77,8 +77,8 @@ class BlockLogFormatter extends LogFormatter {
                        // block restrictions
                        if ( isset( $params[6] ) ) {
                                $pages = $params[6]['pages'] ?? [];
-                               $pages = array_map( function ( $page ){
-                                       return $this->makePageLink( Title::newFromText( ( $page ) ) );
+                               $pages = array_map( function ( $page ) {
+                                       return $this->makePageLink( Title::newFromText( $page ) );
                                }, $pages );
 
                                $namespaces = $params[6]['namespaces'] ?? [];
index 76a7760..4d1b855 100644 (file)
@@ -24,7 +24,6 @@
  * @author Luke Welling lwelling@wikimedia.org
  */
 
-use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
 
 /**
@@ -89,33 +88,6 @@ class EmailNotification {
                return $this->pageStatus;
        }
 
-       /**
-        * @deprecated since 1.27 use WatchedItemStore::updateNotificationTimestamp directly
-        *
-        * @param User $editor The editor that triggered the update.  Their notification
-        *  timestamp will not be updated(they have already seen it)
-        * @param LinkTarget $linkTarget The link target of the title to update timestamps for
-        * @param string $timestamp Set the update timestamp to this value
-        *
-        * @return int[] Array of user IDs
-        */
-       public static function updateWatchlistTimestamp(
-               User $editor,
-               LinkTarget $linkTarget,
-               $timestamp
-       ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               $config = RequestContext::getMain()->getConfig();
-               if ( !$config->get( 'EnotifWatchlist' ) && !$config->get( 'ShowUpdatedMarker' ) ) {
-                       return [];
-               }
-               return MediaWikiServices::getInstance()->getWatchedItemStore()->updateNotificationTimestamp(
-                       $editor,
-                       $linkTarget,
-                       $timestamp
-               );
-       }
-
        /**
         * Send emails corresponding to the user $editor editing the page $title.
         *
index 63a3eb0..5d7030b 100644 (file)
@@ -82,7 +82,7 @@ class UserMailer {
        static function makeMsgId() {
                global $wgSMTP, $wgServer;
 
-               $domainId = WikiMap::getCurrentWikiDomain()->getId();
+               $domainId = WikiMap::getCurrentWikiDbDomain()->getId();
                $msgid = uniqid( $domainId . ".", true /** for cygwin */ );
                if ( is_array( $wgSMTP ) && isset( $wgSMTP['IDHost'] ) && $wgSMTP['IDHost'] ) {
                        $domain = $wgSMTP['IDHost'];
index cb6249c..14d63a1 100644 (file)
@@ -154,7 +154,7 @@ class ObjectCache {
                        return $keyspace;
                }
 
-               return WikiMap::getCurrentWikiDomain()->getId();
+               return WikiMap::getCurrentWikiDbDomain()->getId();
        }
 
        /**
index 9b5e5a5..983f069 100644 (file)
@@ -1946,15 +1946,13 @@ class Article implements Page {
 
                // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-               // Unicode codepoints (or 255 UTF-8 bytes for old schema).
-               $conf = $this->getContext()->getConfig();
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+               // Unicode codepoints.
                $fields[] = new OOUI\FieldLayout(
                        new OOUI\TextInputWidget( [
                                'name' => 'wpReason',
                                'inputId' => 'wpReason',
                                'tabIndex' => 2,
-                               'maxLength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                               'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                'infusable' => true,
                                'value' => $reason,
                                'autofocus' => true,
index ae32f84..66804bc 100644 (file)
@@ -20,6 +20,7 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\ResultWrapper;
 
 /**
@@ -287,20 +288,27 @@ class ImagePage extends Article {
                return parent::getEmptyPageParserOutput( $options );
        }
 
+       /**
+        * Returns language code to be used for dispaying the image, based on request context and
+        * languages available in the file.
+        *
+        * @param WebRequest $request
+        * @param File $file
+        * @return string|null
+        */
        private function getLanguageForRendering( WebRequest $request, File $file ) {
-               $handler = $this->displayImg->getHandler();
+               $handler = $file->getHandler();
                if ( !$handler ) {
                        return null;
                }
 
-               $requestLanguage = $request->getVal( 'lang' );
-               if ( !is_null( $requestLanguage ) ) {
-                       if ( $handler->validateParam( 'lang', $requestLanguage ) ) {
-                               return $requestLanguage;
-                       }
+               $config = MediaWikiServices::getInstance()->getMainConfig();
+               $requestLanguage = $request->getVal( 'lang', $config->get( 'LanguageCode' ) );
+               if ( $handler->validateParam( 'lang', $requestLanguage ) ) {
+                       return $file->getMatchedLanguage( $requestLanguage );
                }
 
-               return $handler->getDefaultRenderLanguage( $this->displayImg );
+               return $handler->getDefaultRenderLanguage( $file );
        }
 
        protected function openShowImage() {
@@ -349,7 +357,7 @@ class ImagePage extends Article {
                                # image
                                # "Download high res version" link below the image
                                # $msgsize = $this->getContext()->msg( 'file-info-size', $width_orig, $height_orig,
-                               #   Linker::formatSize( $this->displayImg->getSize() ), $mime )->escaped();
+                               #   Language::formatSize( $this->displayImg->getSize() ), $mime )->escaped();
                                # We'll show a thumbnail of this image
                                if ( $width > $maxWidth || $height > $maxHeight || $this->displayImg->isVectorized() ) {
                                        list( $width, $height ) = $this->getDisplayWidthHeight(
index b1bf7bd..072c3c6 100644 (file)
@@ -2815,8 +2815,7 @@ class WikiPage implements Page, IDBAccessObject {
         */
        protected function archiveRevisions( $dbw, $id, $suppress ) {
                global $wgContentHandlerUseDB, $wgMultiContentRevisionSchemaMigrationStage,
-                       $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage,
-                       $wgDeleteRevisionsBatchSize;
+                       $wgActorTableSchemaMigrationStage, $wgDeleteRevisionsBatchSize;
 
                // Given the lock above, we can be confident in the title and page ID values
                $namespace = $this->getTitle()->getNamespace();
@@ -2942,9 +2941,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
 
                        $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
-                       if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
-                               $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
-                       }
+                       $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
                        if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                                $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
                        }
@@ -3033,7 +3030,10 @@ class WikiPage implements Page, IDBAccessObject {
                // Clear caches
                self::onArticleDelete( $this->mTitle );
                ResourceLoaderWikiModule::invalidateModuleCache(
-                       $this->mTitle, $revision, null, wfWikiID()
+                       $this->mTitle,
+                       $revision,
+                       null,
+                       WikiMap::getCurrentWikiDbDomain()->getId()
                );
 
                // Reset this object and the Title object
@@ -3282,7 +3282,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                if ( $wgUseRCPatrol ) {
                        // Mark all reverted edits as patrolled
-                       $set['rc_patrolled'] = RecentChange::PRC_PATROLLED;
+                       $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
                }
 
                if ( count( $set ) ) {
@@ -3506,7 +3506,7 @@ class WikiPage implements Page, IDBAccessObject {
                                // means that some cache invalidations happen that are not strictly needed.
                                $cache->makeGlobalKey(
                                        'interwiki-page',
-                                       WikiMap::getCurrentWikiDomain()->getId(),
+                                       WikiMap::getCurrentWikiDbDomain()->getId(),
                                        $title->getDBkey()
                                )
                        );
index 5f7621e..0fe456d 100644 (file)
@@ -3681,7 +3681,6 @@ class Parser {
                $deps = [];
 
                # Loop to fetch the article, with up to 1 redirect
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
                        # Give extensions a chance to select the revision instead
                        $id = false; # Assume current
@@ -4371,7 +4370,7 @@ class Parser {
                        $anchor = $safeHeadline;
                        $fallbackAnchor = $fallbackHeadline;
                        if ( isset( $refers[$arrayKey] ) ) {
-                               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
+                               // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
                                for ( $i = 2; isset( $refers["${arrayKey}_$i"] ); ++$i );
                                $anchor .= "_$i";
                                $linkAnchor .= "_$i";
@@ -4380,7 +4379,7 @@ class Parser {
                                $refers[$arrayKey] = true;
                        }
                        if ( $fallbackHeadline !== false && isset( $refers[$fallbackArrayKey] ) ) {
-                               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall,Generic.Formatting.DisallowMultipleStatements
+                               // phpcs:ignore Generic.Formatting.DisallowMultipleStatements
                                for ( $i = 2; isset( $refers["${fallbackArrayKey}_$i"] ); ++$i );
                                $fallbackAnchor .= "_$i";
                                $refers["${fallbackArrayKey}_$i"] = true;
index a41e7b6..bf4c098 100644 (file)
@@ -52,7 +52,7 @@ class RemexStripTagHandler implements TokenHandler {
        // (although "block-level" is not technically defined for elements that are
        // new in HTML5).
        // Structured as tag => true to allow O(1) membership test.
-       static private $BLOCK_LEVEL_TAGS = [
+       private static $BLOCK_LEVEL_TAGS = [
                'address' => true,
                'article' => true,
                'aside' => true,
index 5c38159..455130c 100644 (file)
@@ -115,7 +115,7 @@ abstract class Profiler {
         */
        public function getProfileID() {
                if ( $this->profileID === false ) {
-                       return WikiMap::getCurrentWikiDomain()->getId();
+                       return WikiMap::getCurrentWikiDbDomain()->getId();
                } else {
                        return $this->profileID;
                }
index 57bd01f..b00401d 100644 (file)
@@ -440,7 +440,7 @@ class SectionProfiler {
                $level = $stack[$start][1];
                $count = 0;
                for ( $i = $start - 1; $i >= 0 && $stack[$i][1] > $level; $i-- ) {
-                       $count ++;
+                       $count++;
                }
                return $count;
        }
index 71f6854..3736d6d 100644 (file)
@@ -125,7 +125,7 @@ abstract class MachineReadableRCFeedFormatter implements RCFeedFormatter {
                $packet['server_name'] = $wgServerName;
 
                $packet['server_script_path'] = $wgScriptPath ?: '/';
-               $packet['wiki'] = WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() );
+               $packet['wiki'] = WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() );
 
                return $this->formatArray( $packet );
        }
index 07fab78..1d3fd86 100644 (file)
@@ -107,7 +107,7 @@ class ExtensionProcessor implements Processor {
        ];
 
        /**
-        * Things that are not 'attributes', but are not in
+        * Things that are not 'attributes', and are not in
         * $globalSettings or $creditsAttributes.
         *
         * @var array
@@ -119,6 +119,7 @@ class ExtensionProcessor implements Processor {
                'ResourceFileModulePaths',
                'ResourceModules',
                'ResourceModuleSkinStyles',
+               'QUnitTestModule',
                'ExtensionMessagesFiles',
                'MessagesDirs',
                'type',
@@ -394,6 +395,19 @@ class ExtensionProcessor implements Processor {
                                }
                        }
                }
+
+               if ( isset( $info['QUnitTestModule'] ) ) {
+                       $data = $info['QUnitTestModule'];
+                       if ( isset( $data['localBasePath'] ) ) {
+                               if ( $data['localBasePath'] === '' ) {
+                                       // Avoid double slashes (e.g. /extensions/Example//path)
+                                       $data['localBasePath'] = $dir;
+                               } else {
+                                       $data['localBasePath'] = "$dir/{$data['localBasePath']}";
+                               }
+                       }
+                       $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data;
+               }
        }
 
        protected function extractExtensionMessagesFiles( $dir, array $info ) {
index c513aed..b648260 100644 (file)
@@ -408,24 +408,24 @@ class ResourceLoader implements LoggerAwareInterface {
                                . 'Edit your <code>LocalSettings.php</code> to enable it.' );
                }
 
-               // Get core test suites
-               $testModules = [];
-               $testModules['qunit'] = [];
-               // Get other test suites (e.g. from extensions)
+               $testModules = [
+                       'qunit' => [],
+               ];
+
+               // Get test suites from extensions
                // Avoid PHP 7.1 warning from passing $this by reference
                $rl = $this;
                Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
+               $extRegistry = ExtensionRegistry::getInstance();
+               // In case of conflict, the deprecated hook has precedence.
+               $testModules['qunit'] += $extRegistry->getAttribute( 'QUnitTestModules' );
 
-               // Add the testrunner (which configures QUnit) to the dependencies.
-               // Since it must be ready before any of the test suites are executed.
+               // Add the QUnit testrunner as implicit dependency to extension test suites.
                foreach ( $testModules['qunit'] as &$module ) {
-                       // Make sure all test modules are top-loading so that when QUnit starts
-                       // on document-ready, it will run once and finish. If some tests arrive
-                       // later (possibly after QUnit has already finished) they will be ignored.
-                       $module['position'] = 'top';
                        $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
                }
 
+               // Get core test suites
                $testModules['qunit'] =
                        ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
 
@@ -1081,7 +1081,7 @@ MESSAGE;
                                                        // Load scripts raw...
                                                        $strContent = $scripts;
                                                } elseif ( is_array( $scripts ) ) {
-                                                       // ...except when $scripts is an array of URLs
+                                                       // ...except when $scripts is an array of URLs or an associative array
                                                        $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
                                                }
                                                break;
@@ -1202,7 +1202,8 @@ MESSAGE;
         *
         * @param string $name Module name or implement key (format "`[name]@[version]`")
         * @param XmlJsCode|array|string $scripts Code as XmlJsCode (to be wrapped in a closure),
-        *  list of URLs to JavaScript files, or a string of JavaScript for `$.globalEval`.
+        *  list of URLs to JavaScript files, string of JavaScript for `$.globalEval`, or array with
+        *  'files' and 'main' properties (see ResourceLoaderModule::getScript())
         * @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs
         *   to CSS files keyed by media type
         * @param mixed $messages List of messages associated with this module. May either be an
@@ -1217,14 +1218,37 @@ MESSAGE;
                $name, $scripts, $styles, $messages, $templates
        ) {
                if ( $scripts instanceof XmlJsCode ) {
-                       if ( self::inDebugMode() ) {
+                       if ( $scripts->value === '' ) {
+                               $scripts = null;
+                       } elseif ( self::inDebugMode() ) {
                                $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
                        } else {
                                $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
                        }
+               } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
+                       $files = $scripts['files'];
+                       foreach ( $files as $path => &$file ) {
+                               // $file is changed (by reference) from a descriptor array to the content of the file
+                               // All of these essentially do $file = $file['content'];, some just have wrapping around it
+                               if ( $file['type'] === 'script' ) {
+                                       // Multi-file modules only get two parameters ($ and jQuery are being phased out)
+                                       if ( self::inDebugMode() ) {
+                                               $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
+                                       } else {
+                                               $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
+                                       }
+                               } else {
+                                       $file = $file['content'];
+                               }
+                       }
+                       $scripts = XmlJsCode::encodeObject( [
+                               'main' => $scripts['main'],
+                               'files' => XmlJsCode::encodeObject( $files, self::inDebugMode() )
+                       ], self::inDebugMode() );
                } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
                        throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
                }
+
                // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
                // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
                // of "{}". Force them to objects.
@@ -1233,7 +1257,7 @@ MESSAGE;
                        $scripts,
                        (object)$styles,
                        (object)$messages,
-                       (object)$templates,
+                       (object)$templates
                ];
                self::trimArray( $module );
 
index 42bd66a..0e53e5e 100644 (file)
@@ -90,6 +90,21 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         */
        protected $skinStyles = [];
 
+       /**
+        * @var array List of packaged files to make available through require()
+        * @par Usage:
+        * @code
+        * [ [file-path], [file-path], ... ]
+        * @endcode
+        */
+       protected $packageFiles = null;
+
+       /**
+        * @var array Expanded versions of $packageFiles, lazy-computed by expandPackageFiles();
+        *  keyed by context hash
+        */
+       private $expandedPackageFiles = [];
+
        /**
         * @var array List of modules this module depends on
         * @par Usage:
@@ -171,7 +186,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         *         'remoteExtPath' => [base path],
         *         // Equivalent of remoteBasePath, but relative to $wgStylePath
         *         'remoteSkinPath' => [base path],
-        *         // Scripts to always include
+        *         // Scripts to always include (cannot be set if 'packageFiles' is also set, see below)
         *         'scripts' => [file path string or array of file path strings],
         *         // Scripts to include in specific language contexts
         *         'languageScripts' => [
@@ -183,6 +198,19 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         *         ],
         *         // Scripts to include in debug contexts
         *         'debugScripts' => [file path string or array of file path strings],
+        *         // For package modules: files to make available for internal require() use
+        *         // 'type' is optional, and will be inferred from the file name extension if omitted
+        *         // 'config' can only be used when 'type' is 'data'; vars are resolved with Config::get()
+        *         // If 'packageFiles' is set, 'scripts' cannot also be set
+        *         'packageFiles' => [
+        *             [file path string], // or:
+        *             [file alias] => [file path string], // or:
+        *             [file alias] => [ 'file' => [file path string], 'type' => 'script'|'data' ], // or:
+        *             [file alias] => [ 'content' => [string], 'type' => 'script'|'data' ], // or:
+        *             [file alias] => [ 'callback' => [callable], 'type' => 'script'|'data' ], // or:
+        *             [file alias] => [ 'config' => [ [config var name], ... ], 'type' => 'data' ], // or:
+        *             [file alias] => [ 'config' => [ [JS name] => [PHP name] ], 'type' => 'data' ],
+        *         ],
         *         // Modules which must be loaded before this module
         *         'dependencies' => [module name string or array of module name strings],
         *         'templates' => [
@@ -224,6 +252,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                case 'scripts':
                                case 'debugScripts':
                                case 'styles':
+                               case 'packageFiles':
                                        $this->{$member} = (array)$option;
                                        break;
                                case 'templates':
@@ -276,6 +305,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                                        break;
                        }
                }
+               if ( isset( $options['scripts'] ) && isset( $options['packageFiles'] ) ) {
+                       throw new InvalidArgumentException( "A module may not set both 'scripts' and 'packageFiles'" );
+               }
                if ( $hasTemplates ) {
                        $this->dependencies[] = 'mediawiki.template';
                        // Ensure relevant template compiler module gets loaded
@@ -346,11 +378,21 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * Gets all scripts for a given context concatenated together.
         *
         * @param ResourceLoaderContext $context Context in which to generate script
-        * @return string JavaScript code for $context
+        * @return string|array JavaScript code for $context, or package files data structure
         */
        public function getScript( ResourceLoaderContext $context ) {
+               $deprecationScript = $this->getDeprecationInformation();
+               if ( $this->packageFiles !== null ) {
+                       $packageFiles = $this->getPackageFiles( $context );
+                       if ( $deprecationScript ) {
+                               $mainFile =& $packageFiles['files'][ $packageFiles['main'] ];
+                               $mainFile['content'] = $deprecationScript . $mainFile['content'];
+                       }
+                       return $packageFiles;
+               }
+
                $files = $this->getScriptFiles( $context );
-               return $this->getDeprecationInformation() . $this->readScriptFiles( $files );
+               return $deprecationScript . $this->readScriptFiles( $files );
        }
 
        /**
@@ -372,7 +414,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * @return bool
         */
        public function supportsURLLoading() {
-               return $this->debugRaw;
+               // If package files are involved, don't support URL loading, because that breaks
+               // scoped require() functions
+               return $this->debugRaw && !$this->packageFiles;
        }
 
        /**
@@ -507,9 +551,18 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                        $files = array_merge( $files, $styleFiles );
                }
 
+               // Extract file names for package files
+               $expandedPackageFiles = $this->expandPackageFiles( $context );
+               $packageFiles = $expandedPackageFiles ?
+                       array_filter( array_map( function ( $fileInfo ) {
+                               return $fileInfo['filePath'] ?? null;
+                       }, $expandedPackageFiles['files'] ) ) :
+                       [];
+
                // Final merge, this should result in a master list of dependent files
                $files = array_merge(
                        $files,
+                       $packageFiles,
                        $this->scripts,
                        $this->templates,
                        $context->getDebug() ? $this->debugScripts : [],
@@ -568,6 +621,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
 
                $summary[] = [
                        'options' => $options,
+                       'packageFiles' => $this->expandPackageFiles( $context ),
                        'fileHashes' => $this->getFileHashes( $context ),
                        'messageBlob' => $this->getMessageBlob( $context ),
                ];
@@ -615,6 +669,18 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
        }
 
+       /**
+        * Infer the file type from a package file path.
+        * @param string $path
+        * @return string 'script' or 'data'
+        */
+       public static function getPackageFileType( $path ) {
+               if ( preg_match( '/\.json$/i', $path ) ) {
+                       return 'data';
+               }
+               return 'script';
+       }
+
        /**
         * Collates file paths by option (where provided).
         *
@@ -790,7 +856,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
         * Get the contents of a list of JavaScript files. Helper for getScript().
         *
         * @param array $scripts List of file paths to scripts to read, remap and concetenate
-        * @return string Concatenated and remapped JavaScript data from $scripts
+        * @return string Concatenated JavaScript data from $scripts
         * @throws MWException
         */
        private function readScriptFiles( array $scripts ) {
@@ -1010,6 +1076,147 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
                return $templates;
        }
 
+       /**
+        * Expand the packageFiles definition into something that's (almost) the right format for
+        * getPackageFiles() to return. This expands shorthands, resolves config vars and callbacks,
+        * but does not expand file paths or read the actual contents of files. Those things are done
+        * by getPackageFiles().
+        *
+        * This is split up in this way so that getFileHashes() can get a list of file names, and
+        * getDefinitionSummary() can get config vars and callback results in their expanded form.
+        *
+        * @param ResourceLoaderContext $context
+        * @return array|null
+        */
+       private function expandPackageFiles( ResourceLoaderContext $context ) {
+               $hash = $context->getHash();
+               if ( isset( $this->expandedPackageFiles[$hash] ) ) {
+                       return $this->expandedPackageFiles[$hash];
+               }
+               if ( $this->packageFiles === null ) {
+                       return null;
+               }
+               $expandedFiles = [];
+               $mainFile = null;
+
+               foreach ( $this->packageFiles as $alias => $fileInfo ) {
+                       // Alias is optional, but only when specfiying plain file names (strings)
+                       if ( is_int( $alias ) ) {
+                               if ( is_array( $fileInfo ) ) {
+                                       $msg = __METHOD__ . ": invalid package file definition for module " .
+                                               "\"{$this->getName()}\": key is required when value is not a string";
+                                       wfDebugLog( 'resourceloader', $msg );
+                                       throw new MWException( $msg );
+                               }
+                               $alias = $fileInfo;
+                       }
+                       if ( !is_array( $fileInfo ) ) {
+                               $fileInfo = [ 'file' => $fileInfo ];
+                       }
+
+                       // Infer type from alias if needed
+                       $type = $fileInfo['type'] ?? self::getPackageFileType( $alias );
+                       $expanded = [ 'type' => $type ];
+                       if ( !empty( $fileInfo['main'] ) ) {
+                               $mainFile = $alias;
+                               if ( $type !== 'script' ) {
+                                       $msg = __METHOD__ . ": invalid package file definition for module " .
+                                               "\"{$this->getName()}\": main file \"$mainFile\" must be of type \"script\", not \"$type\"";
+                                       wfDebugLog( 'resourceloader', $msg );
+                                       throw new MWException( $msg );
+                               }
+                       }
+
+                       if ( isset( $fileInfo['content'] ) ) {
+                               $expanded['content'] = $fileInfo['content'];
+                       } elseif ( isset( $fileInfo['file'] ) ) {
+                               $expanded['filePath'] = $fileInfo['file'];
+                       } elseif ( isset( $fileInfo['callback'] ) ) {
+                               if ( is_callable( $fileInfo['callback'] ) ) {
+                                       $expanded['content'] = $fileInfo['callback']( $context );
+                               } else {
+                                       $msg = __METHOD__ . ": invalid callback for package file \"$alias\"" .
+                                               " in module \"{$this->getName()}\"";
+                                       wfDebugLog( 'resourceloader', $msg );
+                                       throw new MWException( $msg );
+                               }
+                       } elseif ( isset( $fileInfo['config'] ) ) {
+                               if ( $type !== 'data' ) {
+                                       $msg = __METHOD__ . ": invalid use of \"config\" for package file \"$alias\" in module " .
+                                               "\"{$this->getName()}\": type must be \"data\" but is \"$type\"";
+                                       wfDebugLog( 'resourceloader', $msg );
+                                       throw new MWException( $msg );
+                               }
+                               $expandedConfig = [];
+                               foreach ( $fileInfo['config'] as $key => $var ) {
+                                       $expandedConfig[ is_numeric( $key ) ? $var : $key ] = $this->getConfig()->get( $var );
+                               }
+                               $expanded['content'] = $expandedConfig;
+                       } elseif ( !empty( $fileInfo['main'] ) ) {
+                               // 'foo.js' => [ 'main' => true ] is shorthand
+                               $expanded['filePath'] = $alias;
+                       } else {
+                               $msg = __METHOD__ . ": invalid package file definition for \"$alias\" in module " .
+                                       "\"{$this->getName()}\": one of \"file\", \"content\", \"callback\" or \"config\" must be set";
+                               wfDebugLog( 'resourceloader', $msg );
+                               throw new MWException( $msg );
+                       }
+
+                       $expandedFiles[$alias] = $expanded;
+               }
+
+               if ( $expandedFiles && $mainFile === null ) {
+                       // The first package file that is a script is the main file
+                       foreach ( $expandedFiles as $path => &$file ) {
+                               if ( $file['type'] === 'script' ) {
+                                       $mainFile = $path;
+                                       break;
+                               }
+                       }
+               }
+
+               $result = [
+                       'main' => $mainFile,
+                       'files' => $expandedFiles
+               ];
+
+               $this->expandedPackageFiles[$hash] = $result;
+               return $result;
+       }
+
+       /**
+        * Resolves the package files defintion and generates the content of each package file.
+        * @param ResourceLoaderContext $context
+        * @return array Package files data structure, see ResourceLoaderModule::getScript()
+        */
+       public function getPackageFiles( ResourceLoaderContext $context ) {
+               if ( $this->packageFiles === null ) {
+                       return null;
+               }
+               $expandedPackageFiles = $this->expandPackageFiles( $context );
+
+               // Expand file contents
+               foreach ( $expandedPackageFiles['files'] as &$fileInfo ) {
+                       if ( isset( $fileInfo['filePath'] ) ) {
+                               $localPath = $this->getLocalPath( $fileInfo['filePath'] );
+                               if ( !file_exists( $localPath ) ) {
+                                       $msg = __METHOD__ . ": package file not found: \"$localPath\"" .
+                                               " in module \"{$this->getName()}\"";
+                                       wfDebugLog( 'resourceloader', $msg );
+                                       throw new MWException( $msg );
+                               }
+                               $content = $this->stripBom( file_get_contents( $localPath ) );
+                               if ( $fileInfo['type'] === 'data' ) {
+                                       $content = json_decode( $content );
+                               }
+                               $fileInfo['content'] = $content;
+                               unset( $fileInfo['filePath'] );
+                       }
+               }
+
+               return $expandedPackageFiles;
+       }
+
        /**
         * Takes an input string and removes the UTF-8 BOM character if present
         *
diff --git a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php
deleted file mode 100644 (file)
index 8f4aa3b..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-/**
- * ResourceLoader module for mediawiki.jqueryMsg that provides generated data.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * ResourceLoader module for mediawiki.jqueryMsg and its generated data
- */
-class ResourceLoaderJqueryMsgModule extends ResourceLoaderFileModule {
-
-       /**
-        * @param ResourceLoaderContext $context
-        * @return string JavaScript code
-        */
-       public function getScript( ResourceLoaderContext $context ) {
-               $fileScript = parent::getScript( $context );
-
-               $tagData = Sanitizer::getRecognizedTagData();
-               $allowedHtmlElements = array_merge(
-                       array_keys( $tagData['htmlpairs'] ),
-                       array_diff(
-                               array_keys( $tagData['htmlsingle'] ),
-                               array_keys( $tagData['htmlsingleonly'] )
-                       )
-               );
-
-               $magicWords = [
-                       'SITENAME' => $this->getConfig()->get( 'Sitename' ),
-               ];
-               Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] );
-
-               $parserDefaults = [
-                       'allowedHtmlElements' => $allowedHtmlElements,
-                       'magic' => $magicWords,
-               ];
-
-               $setDataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [
-                       $parserDefaults,
-                       // Pass deep=true because mediawiki.jqueryMsg.js contains
-                       // page-specific magic words that must not be overwritten.
-                       true,
-               ] );
-
-               return $fileScript . $setDataScript;
-       }
-
-       /**
-        * @param ResourceLoaderContext $context
-        * @return array
-        */
-       public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
-               // Bypass file module urls
-               return ResourceLoaderModule::getScriptURLsForDebug( $context );
-       }
-
-       /**
-        * @return bool
-        */
-       public function enableModuleContentVersion() {
-               return true;
-       }
-}
diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
deleted file mode 100644 (file)
index eb09664..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-/**
- * ResourceLoader module for providing language names.
- *
- * By default these names will be autonyms however other extensions may
- * provided language names in the context language (e.g. cldr extension)
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Ed Sanders
- * @author Trevor Parscal
- */
-
-/**
- * ResourceLoader module for populating language specific data.
- */
-class ResourceLoaderLanguageNamesModule extends ResourceLoaderFileModule {
-
-       protected $targets = [ 'desktop', 'mobile' ];
-
-       /**
-        * @param ResourceLoaderContext $context
-        * @return array
-        */
-       protected function getData( ResourceLoaderContext $context ) {
-               return Language::fetchLanguageNames(
-                       $context->getLanguage(),
-                       'all'
-               );
-       }
-
-       /**
-        * @param ResourceLoaderContext $context
-        * @return string JavaScript code
-        */
-       public function getScript( ResourceLoaderContext $context ) {
-               return Xml::encodeJsCall(
-                       'mw.language.setData',
-                       [
-                               $context->getLanguage(),
-                               'languageNames',
-                               $this->getData( $context )
-                       ],
-                       ResourceLoader::inDebugMode()
-               );
-       }
-
-       /**
-        * @param ResourceLoaderContext|null $context
-        * @return array
-        */
-       public function getDependencies( ResourceLoaderContext $context = null ) {
-               return [ 'mediawiki.language' ];
-       }
-
-       /**
-        * @return bool
-        */
-       public function enableModuleContentVersion() {
-               return true;
-       }
-
-}
diff --git a/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php b/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php
deleted file mode 100644 (file)
index d16a4ff..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-/**
- * ResourceLoader mediawiki.util module
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * ResourceLoader module for mediawiki.util
- *
- * @since 1.30
- */
-class ResourceLoaderMediaWikiUtilModule extends ResourceLoaderFileModule {
-       /**
-        * @inheritDoc
-        */
-       public function getScript( ResourceLoaderContext $context ) {
-               return ResourceLoader::makeConfigSetScript(
-                               [ 'wgFragmentMode' => $this->getConfig()->get( 'FragmentMode' ) ]
-                       )
-                       . "\n"
-                       . parent::getScript( $context );
-       }
-
-       /**
-        * @inheritDoc
-        */
-       public function supportsURLLoading() {
-               return false;
-       }
-
-       /**
-        * @inheritDoc
-        */
-       public function enableModuleContentVersion() {
-               return true;
-       }
-}
index 30b2aa7..ae79dda 100644 (file)
@@ -159,8 +159,20 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
         * Get all JS for this module for a given language and skin.
         * Includes all relevant JS except loader scripts.
         *
+        * For "plain" script modules, this should return a string with JS code. For multi-file modules
+        * where require() is used to load one file from another file, this should return an array
+        * structured as follows:
+        * [
+        *     'files' => [
+        *         'file1.js' => [ 'type' => 'script', 'content' => 'JS code' ],
+        *         'file2.js' => [ 'type' => 'script', 'content' => 'JS code' ],
+        *         'data.json' => [ 'type' => 'data', 'content' => array ]
+        *     ],
+        *     'main' => 'file1.js'
+        * ]
+        *
         * @param ResourceLoaderContext $context
-        * @return string JavaScript code
+        * @return string|array JavaScript code (string), or multi-file structure described above (array)
         */
        public function getScript( ResourceLoaderContext $context ) {
                // Stub, override expected
@@ -691,7 +703,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface {
 
                // This MUST build both scripts and styles, regardless of whether $context->getOnly()
                // is 'scripts' or 'styles' because the result is used by getVersionHash which
-               // must be consistent regardles of the 'only' filter on the current request.
+               // must be consistent regardless of the 'only' filter on the current request.
                // Also, when introducing new module content resources (e.g. templates, headers),
                // these should only be included in the array when they are non-empty so that
                // existing modules not using them do not get their cache invalidated.
diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php
deleted file mode 100644 (file)
index 0ad7fe4..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-/**
- * ResourceLoader module for populating special characters data for some
- * editing extensions to use.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * ResourceLoader module for populating special characters data for some
- * editing extensions to use.
- */
-class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule {
-       private $path = "resources/src/mediawiki.language/specialcharacters.json";
-       protected $targets = [ 'desktop', 'mobile' ];
-
-       /**
-        * Get all the dynamic data.
-        *
-        * @return array
-        */
-       protected function getData() {
-               global $IP;
-               return json_decode( file_get_contents( "$IP/{$this->path}" ) );
-       }
-
-       /**
-        * @param ResourceLoaderContext $context
-        * @return string JavaScript code
-        */
-       public function getScript( ResourceLoaderContext $context ) {
-               return Xml::encodeJsCall(
-                       'mw.language.setSpecialCharacters',
-                       [
-                               $this->getData()
-                       ],
-                       ResourceLoader::inDebugMode()
-               );
-       }
-
-       /**
-        * @return bool
-        */
-       public function enableModuleContentVersion() {
-               return true;
-       }
-
-       /**
-        * @param ResourceLoaderContext|null $context
-        * @return array
-        */
-       public function getDependencies( ResourceLoaderContext $context = null ) {
-               return [ 'mediawiki.language' ];
-       }
-
-       /**
-        * @return array
-        */
-       public function getMessages() {
-               return [
-                       'special-characters-group-latin',
-                       'special-characters-group-latinextended',
-                       'special-characters-group-ipa',
-                       'special-characters-group-symbols',
-                       'special-characters-group-greek',
-                       'special-characters-group-greekextended',
-                       'special-characters-group-cyrillic',
-                       'special-characters-group-arabic',
-                       'special-characters-group-arabicextended',
-                       'special-characters-group-persian',
-                       'special-characters-group-hebrew',
-                       'special-characters-group-bangla',
-                       'special-characters-group-tamil',
-                       'special-characters-group-telugu',
-                       'special-characters-group-sinhala',
-                       'special-characters-group-devanagari',
-                       'special-characters-group-gujarati',
-                       'special-characters-group-thai',
-                       'special-characters-group-lao',
-                       'special-characters-group-khmer',
-                       'special-characters-group-canadianaboriginal',
-                       'special-characters-title-endash',
-                       'special-characters-title-emdash',
-                       'special-characters-title-minus'
-               ];
-       }
-}
index b5d31ef..334fc73 100644 (file)
@@ -75,7 +75,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                }
 
                $illegalFileChars = $conf->get( 'IllegalFileChars' );
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
 
                // Build list of variables
                $skin = $context->getSkin();
@@ -123,8 +122,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
                        'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
                        'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
                        'wgEnableUploads' => $conf->get( 'EnableUploads' ),
-                       'wgCommentByteLimit' => $oldCommentSchema ? 255 : null,
-                       'wgCommentCodePointLimit' => $oldCommentSchema ? null : CommentStore::COMMENT_CHARACTER_LIMIT,
+                       'wgCommentByteLimit' => null,
+                       'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
                ];
 
                Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars, $skin ] );
diff --git a/includes/resourceloader/ResourceLoaderUploadDialogModule.php b/includes/resourceloader/ResourceLoaderUploadDialogModule.php
deleted file mode 100644 (file)
index 1a390cf..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-/**
- * ResourceLoader module for the upload dialog configuration data.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * ResourceLoader module for the upload dialog configuration data.
- *
- * @since 1.27
- */
-class ResourceLoaderUploadDialogModule extends ResourceLoaderModule {
-
-       protected $targets = [ 'desktop', 'mobile' ];
-
-       /**
-        * @param ResourceLoaderContext $context
-        * @return string JavaScript code
-        */
-       public function getScript( ResourceLoaderContext $context ) {
-               $config = $context->getResourceLoader()->getConfig();
-               return ResourceLoader::makeConfigSetScript( [
-                       'wgUploadDialog' => $config->get( 'UploadDialog' ),
-               ] );
-       }
-
-       /**
-        * @return bool
-        */
-       public function enableModuleContentVersion() {
-               return true;
-       }
-}
index fe77576..ecb1a09 100644 (file)
@@ -528,15 +528,15 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
         * @param Title $title
         * @param Revision|null $old Prior page revision
         * @param Revision|null $new New page revision
-        * @param string $wikiId
+        * @param string $domain Database domain ID
         * @since 1.28
         */
        public static function invalidateModuleCache(
-               Title $title, Revision $old = null, Revision $new = null, $wikiId
+               Title $title, Revision $old = null, Revision $new = null, $domain
        ) {
                static $formats = [ CONTENT_FORMAT_CSS, CONTENT_FORMAT_JAVASCRIPT ];
 
-               Assert::parameterType( 'string', $wikiId, '$wikiId' );
+               Assert::parameterType( 'string', $domain, '$domain' );
 
                // TODO: MCR: differentiate between page functionality and content model!
                //       Not all pages containing CSS or JS have to be modules! [PageType]
@@ -550,7 +550,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
 
                if ( $purge ) {
                        $cache = ObjectCache::getMainWANInstance();
-                       $key = $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $wikiId );
+                       $key = $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $domain );
                        $cache->touchCheckKey( $key );
                }
        }
index 36198cd..bf28f60 100644 (file)
@@ -75,7 +75,7 @@ class RevDelLogItem extends RevDelItem {
                $dbw->update( 'recentchanges',
                        [
                                'rc_deleted' => $bits,
-                               'rc_patrolled' => RecentChange::PRC_PATROLLED
+                               'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED
                        ],
                        [
                                'rc_logid' => $this->row->log_id,
index 2cfa2ab..6eb0b37 100644 (file)
@@ -94,7 +94,7 @@ class RevDelRevisionItem extends RevDelItem {
                $dbw->update( 'recentchanges',
                        [
                                'rc_deleted' => $bits,
-                               'rc_patrolled' => RecentChange::PRC_PATROLLED
+                               'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED
                        ],
                        [
                                'rc_this_oldid' => $this->revision->getId(), // condition
index 34b624f..f7f7e89 100644 (file)
@@ -35,13 +35,14 @@ class RevisionDeleteUser {
 
        /**
         * Update *_deleted bitfields in various tables to hide or unhide usernames
+        *
         * @param string $name Username
         * @param int $userId User id
         * @param string $op Operator '|' or '&'
         * @param null|IDatabase $dbw If you happen to have one lying around
-        * @return bool
+        * @return bool True on success, false on failure (e.g. invalid user ID)
         */
-       private static function setUsernameBitfields( $name, $userId, $op, $dbw ) {
+       private static function setUsernameBitfields( $name, $userId, $op, IDatabase $dbw = null ) {
                global $wgActorTableSchemaMigrationStage;
 
                if ( !$userId || ( $op !== '|' && $op !== '&' ) ) {
@@ -58,7 +59,7 @@ class RevisionDeleteUser {
                # The same goes for the sysop-restricted *_deleted bit.
                $delUser = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
                $delAction = LogPage::DELETED_ACTION | Revision::DELETED_RESTRICTED;
-               if ( $op == '&' ) {
+               if ( $op === '&' ) {
                        $delUser = $dbw->bitNot( $delUser );
                        $delAction = $dbw->bitNot( $delAction );
                }
@@ -194,17 +195,29 @@ class RevisionDeleteUser {
                return true;
        }
 
-       private static function buildSetBitDeletedField( $field, $op, $value, $dbw ) {
+       private static function buildSetBitDeletedField( $field, $op, $value, IDatabase $dbw ) {
                return $field . ' = ' . ( $op === '&'
                        ? $dbw->bitAnd( $field, $value )
                        : $dbw->bitOr( $field, $value ) );
        }
 
-       public static function suppressUserName( $name, $userId, $dbw = null ) {
+       /**
+        * @param string $name User name
+        * @param int $userId Both user name and ID must be provided
+        * @param IDatabase|null $dbw If you happen to have one lying around
+        * @return bool True on success, false on failure (e.g. invalid user ID)
+        */
+       public static function suppressUserName( $name, $userId, IDatabase $dbw = null ) {
                return self::setUsernameBitfields( $name, $userId, '|', $dbw );
        }
 
-       public static function unsuppressUserName( $name, $userId, $dbw = null ) {
+       /**
+        * @param string $name User name
+        * @param int $userId Both user name and ID must be provided
+        * @param IDatabase|null $dbw If you happen to have one lying around
+        * @return bool True on success, false on failure (e.g. invalid user ID)
+        */
+       public static function unsuppressUserName( $name, $userId, IDatabase $dbw = null ) {
                return self::setUsernameBitfields( $name, $userId, '&', $dbw );
        }
 }
index a0100ca..0b29dd7 100644 (file)
@@ -279,18 +279,6 @@ abstract class SearchEngine {
                return static::defaultNearMatcher()->getNearMatch( $searchterm );
        }
 
-       /**
-        * Do a near match (see SearchEngine::getNearMatch) and wrap it into a
-        * SearchResultSet.
-        * @deprecated since 1.27; Use SearchEngine::getNearMatcher()
-        * @param string $searchterm
-        * @return SearchResultSet
-        */
-       public static function getNearMatchResultSet( $searchterm ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               return static::defaultNearMatcher()->getNearMatchResultSet( $searchterm );
-       }
-
        /**
         * Get chars legal for search
         * NOTE: usage as static is deprecated and preserved only as BC measure
index ceb9ceb..385cc35 100644 (file)
@@ -377,23 +377,6 @@ final class SessionManager implements SessionManagerInterface {
         * @{
         */
 
-       /**
-        * Auto-create the given user, if necessary
-        * @private Don't call this yourself. Let Setup.php do it for you at the right time.
-        * @deprecated since 1.27, use MediaWiki\Auth\AuthManager::autoCreateUser instead
-        * @param User $user User to auto-create
-        * @return bool Success
-        * @codeCoverageIgnore
-        */
-       public static function autoCreateUser( User $user ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
-                       $user,
-                       \MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION,
-                       false
-               )->isGood();
-       }
-
        /**
         * Prevent future sessions for the user
         *
diff --git a/includes/site/SiteSQLStore.php b/includes/site/SiteSQLStore.php
deleted file mode 100644 (file)
index b106d11..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-/**
- * Dummy class for accessing the global SiteStore instance.
- *
- * 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
- *
- * @since 1.21
- *
- * @file
- * @ingroup Site
- *
- * @license GPL-2.0-or-later
- * @author Daniel Kinzler
- */
-class SiteSQLStore {
-
-       /**
-        * Returns the global SiteStore instance. This is a relict of the first implementation
-        * of SiteStore, and is kept around for compatibility.
-        *
-        * @note This does not return an instance of SiteSQLStore!
-        *
-        * @since 1.21
-        * @deprecated since 1.27 use MediaWikiServices::getSiteStore()
-        *             or MediaWikiServices::getSiteLookup() instead.
-        *
-        * @param null $sitesTable IGNORED
-        * @param BagOStuff|null $cache IGNORED
-        *
-        * @return SiteStore
-        */
-       public static function newInstance( $sitesTable = null, BagOStuff $cache = null ) {
-               wfDeprecated( __METHOD__, '1.27' );
-               if ( $sitesTable !== null ) {
-                       throw new InvalidArgumentException(
-                               __METHOD__ . ': $sitesTable parameter is unused and must be null'
-                       );
-               }
-
-               // NOTE: we silently ignore $cache for now, since some existing callers
-               // specify it. If we break compatibility with them, we could just as
-               // well just remove this class.
-
-               return \MediaWiki\MediaWikiServices::getInstance()->getSiteStore();
-       }
-
-}
index a71daa0..02247bd 100644 (file)
@@ -46,7 +46,7 @@ abstract class BaseTemplate extends QuickTemplate {
        /**
         * @param string $str
         * @warning You should never use this method. I18n messages should be escaped
-        * @deprecated 1.32 Use ->msg() or ->msgWiki() instead.
+        * @deprecated 1.32 Use ->msg() or ->getMsg() instead.
         * @suppress SecurityCheck-XSS
         * @return-taint exec_html
         */
@@ -55,7 +55,11 @@ abstract class BaseTemplate extends QuickTemplate {
                echo $this->getMsg( $str )->text();
        }
 
+       /**
+        * @deprecated since 1.33 Use ->msg() or ->getMsg() instead.
+        */
        function msgWiki( $str ) {
+               // TODO: Add wfDeprecated( __METHOD__, '1.33' ) after 1.33 got released
                echo $this->getMsg( $str )->parseAsBlock();
        }
 
index 06d0f7b..1e688eb 100644 (file)
@@ -129,7 +129,7 @@ abstract class QuickTemplate {
         * @private
         * @param string $msgKey
         * @warning You should never use this method. I18n messages should be escaped
-        * @deprecated 1.32 Use ->msg() or ->msgWiki() instead.
+        * @deprecated 1.32 Use ->msg() instead.
         * @suppress SecurityCheck-XSS
         * @return-taint exec_html
         */
@@ -140,10 +140,11 @@ abstract class QuickTemplate {
 
        /**
         * An ugly, ugly hack.
-        * @private
+        * @deprecated since 1.33 Use ->msg() instead.
         * @param string $msgKey
         */
        function msgWiki( $msgKey ) {
+               // TODO: Add wfDeprecated( __METHOD__, '1.33' ) after 1.33 got released
                global $wgOut;
 
                $text = wfMessage( $msgKey )->plain();
index 4201f80..4e23777 100644 (file)
@@ -792,10 +792,6 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        $out->addJsConfigVars( 'wgStructuredChangeFiltersMessages', $messages );
                        $out->addJsConfigVars( 'wgStructuredChangeFiltersCollapsedState', $collapsed );
 
-                       $out->addJsConfigVars(
-                               'wgRCFiltersChangeTags',
-                               $this->getChangeTagList()
-                       );
                        $out->addJsConfigVars(
                                'StructuredChangeFiltersDisplayConfig',
                                [
@@ -823,26 +819,35 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                'wgStructuredChangeFiltersCollapsedPreferenceName',
                                static::$collapsedPreferenceName
                        );
-
-                       $out->addJsConfigVars(
-                               'StructuredChangeFiltersLiveUpdatePollingRate',
-                               $this->getConfig()->get( 'StructuredChangeFiltersLiveUpdatePollingRate' )
-                       );
                } else {
                        $out->addBodyClasses( 'mw-rcfilters-disabled' );
                }
        }
 
+       /**
+        * Get config vars to export with the mediawiki.rcfilters.filters.ui module.
+        *
+        * @param ResourceLoaderContext $context
+        * @return array
+        */
+       public static function getRcFiltersConfigVars( ResourceLoaderContext $context ) {
+               return [
+                       'RCFiltersChangeTags' => self::getChangeTagList( $context ),
+                       'StructuredChangeFiltersEditWatchlistUrl' =>
+                               SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
+               ];
+       }
+
        /**
         * Fetch the change tags list for the front end
         *
+        * @param ResourceLoaderContext $context
         * @return array Tag data
         */
-       protected function getChangeTagList() {
+       protected static function getChangeTagList( ResourceLoaderContext $context ) {
                $cache = ObjectCache::getMainWANInstance();
-               $context = $this->getContext();
                return $cache->getWithSetCallback(
-                       $cache->makeKey( 'changeslistspecialpage-changetags', $context->getLanguage()->getCode() ),
+                       $cache->makeKey( 'changeslistspecialpage-changetags', $context->getLanguage() ),
                        $cache::TTL_MINUTE * 10,
                        function () use ( $context ) {
                                $explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
@@ -858,6 +863,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                */
                                $tagHitCounts = array_merge( $explicitlyDefinedTags, $softwareActivatedTags );
 
+                               // HACK work around ChangeTags::truncateTagDescription() requiring a RequestContext
+                               $fakeContext = new RequestContext;
+                               $fakeContext->setLanguage( Language::factory( $context->getLanguage() ) );
+
                                // Build the list and data
                                $result = [];
                                foreach ( $tagHitCounts as $tagName => $hits ) {
@@ -873,7 +882,9 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                        ),
                                                        'description' =>
                                                                ChangeTags::truncateTagDescription(
-                                                                       $tagName, self::TAG_DESC_CHARACTER_LIMIT, $context
+                                                                       $tagName,
+                                                                       self::TAG_DESC_CHARACTER_LIMIT,
+                                                                       $fakeContext
                                                                ),
                                                        'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
                                                        'hits' => $hits,
index 48dd405..743a5a5 100644 (file)
@@ -491,10 +491,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                $wgUser = $user;
                $context->setUser( $user );
 
-               $code = $this->getRequest()->getVal( 'uselang', $user->getOption( 'language' ) );
-               $userLang = Language::factory( $code );
-               $wgLang = $userLang;
-               $context->setLanguage( $userLang );
+               $wgLang = $context->getLanguage();
        }
 
        /**
index 16a6d30..579ca19 100644 (file)
@@ -699,7 +699,6 @@ abstract class QueryPage extends SpecialPage {
 
                        # $res might contain the whole 1,000 rows, so we read up to
                        # $num [should update this to use a Pager]
-                       // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                        for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
                                $line = $this->formatResult( $skin, $row );
                                if ( $line ) {
index bab3c8c..81b82d9 100644 (file)
@@ -148,7 +148,6 @@ class SpecialBlock extends FormSpecialPage {
                $suggestedDurations = self::getSuggestedDurations();
 
                $conf = $this->getConfig();
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
                $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
 
                $a = [];
@@ -243,8 +242,8 @@ class SpecialBlock extends FormSpecialPage {
                        'type' => 'selectandother',
                        // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                        // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-                       // Unicode codepoints (or 255 UTF-8 bytes for old schema).
-                       'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                       // Unicode codepoints.
+                       'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                        'maxlength-unit' => 'codepoints',
                        'options-message' => 'ipbreason-dropdown',
                        'section' => 'reason',
@@ -739,6 +738,9 @@ class SpecialBlock extends FormSpecialPage {
 
                $performer = $context->getUser();
                $enablePartialBlocks = $context->getConfig()->get( 'EnablePartialBlocks' );
+               $isPartialBlock = $enablePartialBlocks &&
+                       isset( $data['EditingRestriction'] ) &&
+                       $data['EditingRestriction'] === 'partial';
 
                // Handled by field validator callback
                // self::validateTargetField( $data['Target'] );
@@ -816,6 +818,10 @@ class SpecialBlock extends FormSpecialPage {
                                return [ 'badaccess-group0' ];
                        }
 
+                       if ( $isPartialBlock ) {
+                               return [ 'ipb_hide_partial' ];
+                       }
+
                        # Recheck params here...
                        if ( $type != Block::TYPE_USER ) {
                                $data['HideUser'] = false; # IP users should not be hidden
@@ -847,12 +853,8 @@ class SpecialBlock extends FormSpecialPage {
                $block->isAutoblocking( $data['AutoBlock'] );
                $block->mHideName = $data['HideUser'];
 
-               if (
-                       $enablePartialBlocks &&
-                       isset( $data['EditingRestriction'] ) &&
-                       $data['EditingRestriction'] === 'partial'
-                ) {
-                        $block->isSitewide( false );
+               if ( $isPartialBlock ) {
+                       $block->isSitewide( false );
                }
 
                $reason = [ 'hookaborted' ];
@@ -868,7 +870,7 @@ class SpecialBlock extends FormSpecialPage {
                                        $title = Title::newFromText( $text );
                                        // Use the link cache since the title has already been loaded when
                                        // the field was validated.
-                                       $restriction = new PageRestriction( 0, $title->getArticleId() );
+                                       $restriction = new PageRestriction( 0, $title->getArticleID() );
                                        $restriction->setTitle( $title );
                                        return $restriction;
                                }, explode( "\n", $data['PageRestrictions'] ) );
index 6198a84..02b72dd 100644 (file)
@@ -222,7 +222,6 @@ class SpecialEditTags extends UnlistedSpecialPage {
                $numRevisions = 0;
                // Live revisions...
                $list = $this->getList();
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $list->reset(); $list->current(); $list->next() ) {
                        $item = $list->current();
                        $numRevisions++;
@@ -239,9 +238,6 @@ class SpecialEditTags extends UnlistedSpecialPage {
 
                // Show form if the user can submit
                if ( $this->isAllowed ) {
-                       $conf = $this->getConfig();
-                       $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
-
                        $form = Xml::openElement( 'form', [ 'method' => 'post',
                                        'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
                                        'id' => 'mw-revdel-form-revisions' ] ) .
@@ -258,9 +254,9 @@ class SpecialEditTags extends UnlistedSpecialPage {
                                                        'id' => 'wpReason',
                                                        // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                                                        // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-                                                       // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+                                                       // Unicode codepoints.
                                                        // "- 155" is to leave room for the auto-generated part of the log entry.
-                                                       'maxlength' => $oldCommentSchema ? 100 : CommentStore::COMMENT_CHARACTER_LIMIT - 155,
+                                                       'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
                                                ] ) .
                                        '</td>' .
                                "</tr><tr>\n" .
@@ -314,7 +310,6 @@ class SpecialEditTags extends UnlistedSpecialPage {
                        // Otherwise, use a multi-select field for adding tags, and a list of
                        // checkboxes for removing them
 
-                       // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                        for ( $list->reset(); $list->current(); $list->next() ) {
                                $currentTags = $list->current()->getTags();
                                if ( $currentTags ) {
index 873decb..a56a745 100644 (file)
@@ -261,7 +261,10 @@ class MediaStatisticsPage extends QueryPage {
         * @param string $mediaType
         */
        protected function outputTableStart( $mediaType ) {
-               $this->getOutput()->addHTML(
+               $out = $this->getOutput();
+               $out->addModuleStyles( 'jquery.tablesorter.styles' );
+               $out->addModules( 'jquery.tablesorter' );
+               $out->addHTML(
                        Html::openElement(
                                'table',
                                [ 'class' => [
index 599ab31..0234507 100644 (file)
@@ -333,14 +333,12 @@ class MovePageForm extends UnlistedSpecialPage {
 
                // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-               // Unicode codepoints (or 255 UTF-8 bytes for old schema).
-               $conf = $this->getConfig();
-               $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+               // Unicode codepoints.
                $fields[] = new OOUI\FieldLayout(
                        new OOUI\TextInputWidget( [
                                'name' => 'wpReason',
                                'id' => 'wpReason',
-                               'maxLength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                               'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                'infusable' => true,
                                'value' => $this->reason,
                        ] ),
index 8051b0b..2bbc0bf 100644 (file)
@@ -192,9 +192,12 @@ class SpecialNewpages extends IncludableSpecialPage {
                // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
                // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
                // to 0 or 1.
-               $changed = array_map( function ( $value ) {
-                       return $value ? '1' : '0';
-               }, $changed );
+               // Also do this only for boolean options, not eg. namespace or tagfilter
+               foreach ( $changed as $key => $value ) {
+                       if ( array_key_exists( $key, $filters ) ) {
+                               $changed[$key] = $changed[$key] ? '1' : '0';
+                       }
+               }
 
                $self = $this->getPageTitle();
                $linkRenderer = $this->getLinkRenderer();
index 50f3710..b462ce5 100644 (file)
@@ -388,7 +388,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
                $numRevisions = 0;
                // Live revisions...
                $list = $this->getList();
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $list->reset(); $list->current(); $list->next() ) {
                        $item = $list->current();
 
@@ -421,9 +420,6 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
                        $out->addModules( [ 'mediawiki.special.revisionDelete' ] );
                        $out->addModuleStyles( 'mediawiki.special' );
 
-                       $conf = $this->getConfig();
-                       $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
-
                        $form = Xml::openElement( 'form', [ 'method' => 'post',
                                        'action' => $this->getPageTitle()->getLocalURL( [ 'action' => 'submit' ] ),
                                        'id' => 'mw-revdel-form-revisions' ] ) .
@@ -450,9 +446,9 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
                                                        'id' => 'wpReason',
                                                        // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                                                        // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-                                                       // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+                                                       // Unicode codepoints.
                                                        // "- 155" is to leave room for the 'wpRevDeleteReasonList' value.
-                                                       'maxlength' => $oldCommentSchema ? 100 : CommentStore::COMMENT_CHARACTER_LIMIT - 155,
+                                                       'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT - 155,
                                                ] ) .
                                        '</td>' .
                                "</tr><tr>\n" .
index fe8fa19..0c74e76 100644 (file)
@@ -165,6 +165,8 @@ class SpecialTags extends SpecialPage {
                        }
                }
 
+               $out->addModuleStyles( 'jquery.tablesorter.styles' );
+               $out->addModules( 'jquery.tablesorter' );
                $out->addHTML( Xml::tags(
                        'table',
                        [ 'class' => 'mw-datatable sortable mw-tags-table' ],
index 4a586b7..1afbb5e 100644 (file)
@@ -40,6 +40,8 @@ class SpecialTrackingCategories extends SpecialPage {
                $this->setHeaders();
                $this->outputHeader();
                $this->getOutput()->allowClickjacking();
+               $this->getOutput()->addModuleStyles( 'jquery.tablesorter.styles' );
+               $this->getOutput()->addModules( 'jquery.tablesorter' );
                $this->getOutput()->addHTML(
                        Html::openElement( 'table', [ 'class' => 'mw-datatable sortable',
                                'id' => 'mw-trackingcategories-table' ] ) . "\n" .
index cd754ca..9654bb7 100644 (file)
@@ -769,9 +769,6 @@ class SpecialUndelete extends SpecialPage {
                                'content' => new OOUI\HtmlSnippet( $this->msg( 'undeleteextrahelp' )->parseAsBlock() )
                        ] );
 
-                       $conf = $this->getConfig();
-                       $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
-
                        $fields[] = new OOUI\FieldLayout(
                                new OOUI\TextInputWidget( [
                                        'name' => 'wpComment',
@@ -781,8 +778,8 @@ class SpecialUndelete extends SpecialPage {
                                        'autofocus' => true,
                                        // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                                        // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-                                       // Unicode codepoints (or 255 UTF-8 bytes for old schema).
-                                       'maxLength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                                       // Unicode codepoints.
+                                       'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                ] ),
                                [
                                        'label' => $this->msg( 'undeletecomment' )->text(),
index a00b031..2da9a41 100644 (file)
@@ -447,10 +447,3 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                return true;
        }
 }
-
-/**
- * @ingroup SpecialPage
- * @ingroup Upload
- */
-class SpecialUploadStashTooLargeException extends UploadStashException {
-}
index 3c2907b..6e6d905 100644 (file)
@@ -714,8 +714,6 @@ class UserrightsPage extends SpecialPage {
                                ->rawParams( $userToolLinks )->parse()
                );
                if ( $canChangeAny ) {
-                       $conf = $this->getConfig();
-                       $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
                        $this->getOutput()->addHTML(
                                $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
                                $grouplist .
@@ -730,8 +728,8 @@ class UserrightsPage extends SpecialPage {
                                                                'id' => 'wpReason',
                                                                // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
                                                                // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
-                                                               // Unicode codepoints (or 255 UTF-8 bytes for old schema).
-                                                               'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                                                               // Unicode codepoints.
+                                                               'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
                                                        ] ) .
                                                "</td>
                                        </tr>
index 0fc6e13..971aa43 100644 (file)
@@ -102,11 +102,6 @@ class SpecialWatchlist extends ChangesListSpecialPage {
 
                if ( $this->isStructuredFilterUiEnabled() ) {
                        $output->addModuleStyles( [ 'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
-
-                       $output->addJsConfigVars(
-                               'wgStructuredChangeFiltersEditWatchlistUrl',
-                               SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
-                       );
                }
        }
 
diff --git a/includes/specials/exception/SpecialUploadStashTooLargeException.php b/includes/specials/exception/SpecialUploadStashTooLargeException.php
new file mode 100644 (file)
index 0000000..6c2e730
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Implements SpecialUploadStashTooLargeException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup SpecialPage
+ * @ingroup Upload
+ */
+class SpecialUploadStashTooLargeException extends UploadStashException {
+}
index fee7740..3fac73c 100644 (file)
@@ -76,58 +76,84 @@ class ActiveUsersPager extends UsersPager {
                return 'qcc_title';
        }
 
-       function getQueryInfo() {
+       function getQueryInfo( $data = null ) {
                $dbr = $this->getDatabase();
 
-               $rcQuery = ActorMigration::newMigration()->getJoin( 'rc_user' );
+               $useActor = (bool)(
+                       $this->getConfig()->get( 'ActorTableSchemaMigrationStage' ) & SCHEMA_COMPAT_READ_NEW
+               );
 
                $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400;
                $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
-               $tables = [ 'querycachetwo', 'user', 'rc' => [ 'recentchanges' ] + $rcQuery['tables'] ];
+               $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
+
+               // Inner subselect to pull the active users out of querycachetwo
+               $tables = [ 'querycachetwo', 'user' ];
+               $fields = [ 'qcc_title', 'user_id' ];
                $jconds = [
                        'user' => [ 'JOIN', 'user_name = qcc_title' ],
-                       'rc' => [ 'JOIN', $rcQuery['fields']['rc_user_text'] . ' = qcc_title' ],
-               ] + $rcQuery['joins'];
+               ];
                $conds = [
                        'qcc_type' => 'activeusers',
                        'qcc_namespace' => NS_USER,
-                       'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
-                       'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes.
-                       'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
-                       'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ),
                ];
+               $options = [];
+               if ( $data !== null ) {
+                       $options['ORDER BY'] = 'qcc_title ' . $data['dir'];
+                       $options['LIMIT'] = $data['limit'];
+                       $conds = array_merge( $conds, $data['conds'] );
+               }
                if ( $this->requestedUser != '' ) {
                        $conds[] = 'qcc_title >= ' . $dbr->addQuotes( $this->requestedUser );
                }
                if ( $this->groups !== [] ) {
-                       $tables[] = 'user_groups';
-                       $jconds['user_groups'] = [ 'JOIN', [ 'ug_user = user_id' ] ];
-                       $conds['ug_group'] = $this->groups;
-                       $conds[] = 'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() );
+                       $tables['ug1'] = 'user_groups';
+                       $jconds['ug1'] = [ 'JOIN', 'ug1.ug_user = user_id' ];
+                       $conds['ug1.ug_group'] = $this->groups;
+                       $conds[] = 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() );
                }
                if ( $this->excludegroups !== [] ) {
-                       foreach ( $this->excludegroups as $group ) {
-                               $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText(
-                                       'user_groups', '1', [
-                                               'ug_user = user_id',
-                                               'ug_group' => $group,
-                                               'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
-                                       ]
-                               ) . ')';
-                       }
+                       $tables['ug2'] = 'user_groups';
+                       $jconds['ug2'] = [ 'LEFT JOIN', [
+                               'ug2.ug_user = user_id',
+                               'ug2.ug_group' => $this->excludegroups,
+                               'ug2.ug_expiry IS NULL OR ug2.ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ),
+                       ] ];
+                       $conds['ug2.ug_user'] = null;
                }
                if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
                        $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText(
                                        'ipblocks', '1', [ 'ipb_user=user_id', 'ipb_deleted' => 1 ]
                                ) . ')';
                }
+               if ( $useActor ) {
+                       $tables[] = 'actor';
+                       $jconds['actor'] = [
+                               'JOIN',
+                               'actor_user = user_id',
+                       ];
+                       $fields[] = 'actor_id';
+               }
+               $subquery = $dbr->buildSelectSubquery( $tables, $fields, $conds, $fname, $options, $jconds );
+
+               // Outer query to select the recent edit counts for the selected active users
+               $tables = [ 'qcc_users' => $subquery, 'recentchanges' ];
+               $jconds = [ 'recentchanges' => [
+                       'JOIN', $useActor ? 'rc_actor = actor_id' : 'rc_user_text = qcc_title',
+               ] ];
+               $conds = [
+                       'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
+                       'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes.
+                       'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
+                       'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ),
+               ];
 
                return [
                        'tables' => $tables,
                        'fields' => [
                                'qcc_title',
                                'user_name' => 'qcc_title',
-                               'user_id' => 'MAX(user_id)',
+                               'user_id' => 'user_id',
                                'recentedits' => 'COUNT(*)'
                        ],
                        'options' => [ 'GROUP BY' => [ 'qcc_title' ] ],
@@ -136,6 +162,36 @@ class ActiveUsersPager extends UsersPager {
                ];
        }
 
+       protected function buildQueryInfo( $offset, $limit, $descending ) {
+               $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
+
+               $sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields );
+               if ( $descending ) {
+                       $orderBy = $sortColumns;
+                       $operator = $this->mIncludeOffset ? '>=' : '>';
+               } else {
+                       $orderBy = [];
+                       foreach ( $sortColumns as $col ) {
+                               $orderBy[] = $col . ' DESC';
+                       }
+                       $operator = $this->mIncludeOffset ? '<=' : '<';
+               }
+               $info = $this->getQueryInfo( [
+                       'limit' => intval( $limit ),
+                       'order' => $descending ? 'DESC' : 'ASC',
+                       'conds' =>
+                               $offset != '' ? [ $this->mIndexField . $operator . $this->mDb->addQuotes( $offset ) ] : [],
+               ] );
+
+               $tables = $info['tables'];
+               $fields = $info['fields'];
+               $conds = $info['conds'];
+               $options = $info['options'];
+               $join_conds = $info['join_conds'];
+               $options['ORDER BY'] = $orderBy;
+               return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
+       }
+
        protected function doBatchLookups() {
                parent::doBatchLookups();
 
index 36391f5..118f439 100644 (file)
@@ -4,7 +4,7 @@
                <meta charset="UTF-8" />
                <title>MediaWiki {{wgVersion}}</title>
                <style media="screen">
-                       html, body {
+                       body {
                                color: #000;
                                background-color: #fff;
                                font-family: sans-serif;
index 58541b2..3e88fcd 100644 (file)
@@ -409,18 +409,3 @@ class UploadFromChunks extends UploadFromFile {
                }
        }
 }
-
-class UploadChunkZeroLengthFileException extends MWException {
-}
-
-class UploadChunkFileException extends MWException {
-}
-
-class UploadChunkVerificationException extends MWException {
-       public $msg;
-       public function __construct( array $res ) {
-               $this->msg = wfMessage( ...$res );
-               parent::__construct( wfMessage( ...$res )
-                       ->inLanguage( 'en' )->useDatabase( false )->text() );
-       }
-}
index babbe3a..aa31a5b 100644 (file)
@@ -766,72 +766,3 @@ class UploadStashFile extends UnregisteredLocalFile {
                return $this->repo->fileExists( $this->path );
        }
 }
-
-/**
- * @ingroup Upload
- */
-class UploadStashException extends MWException implements ILocalizedException {
-       /** @var string|array|MessageSpecifier */
-       protected $messageSpec;
-
-       /**
-        * @param string|array|MessageSpecifier $messageSpec See Message::newFromSpecifier
-        * @param int $code Exception code
-        * @param Exception|Throwable|null $previous The previous exception used for the exception
-        *  chaining.
-        */
-       public function __construct( $messageSpec, $code = 0, $previous = null ) {
-               $this->messageSpec = $messageSpec;
-
-               $msg = $this->getMessageObject()->text();
-               $msg = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $msg );
-               $msg = Sanitizer::stripAllTags( $msg );
-               parent::__construct( $msg, $code, $previous );
-       }
-
-       public function getMessageObject() {
-               return Message::newFromSpecifier( $this->messageSpec );
-       }
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashFileNotFoundException extends UploadStashException {
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashBadPathException extends UploadStashException {
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashFileException extends UploadStashException {
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashZeroLengthFileException extends UploadStashException {
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashNotLoggedInException extends UploadStashException {
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashWrongOwnerException extends UploadStashException {
-}
-
-/**
- * @ingroup Upload
- */
-class UploadStashNoSuchKeyException extends UploadStashException {
-}
diff --git a/includes/upload/exception/UploadChunkFileException.php b/includes/upload/exception/UploadChunkFileException.php
new file mode 100644 (file)
index 0000000..066b38c
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Implements UploadChunkFileException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Upload
+ */
+
+class UploadChunkFileException extends MWException {
+}
diff --git a/includes/upload/exception/UploadChunkVerificationException.php b/includes/upload/exception/UploadChunkVerificationException.php
new file mode 100644 (file)
index 0000000..cee8c03
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Implements UploadChunkVerificationException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Upload
+ */
+
+class UploadChunkVerificationException extends MWException {
+       public $msg;
+       public function __construct( array $res ) {
+               $this->msg = wfMessage( ...$res );
+               parent::__construct( wfMessage( ...$res )
+                       ->inLanguage( 'en' )->useDatabase( false )->text() );
+       }
+}
diff --git a/includes/upload/exception/UploadChunkZeroLengthFileException.php b/includes/upload/exception/UploadChunkZeroLengthFileException.php
new file mode 100644 (file)
index 0000000..9b937b2
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Implements UploadChunkZeroLengthFileException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Upload
+ */
+
+class UploadChunkZeroLengthFileException extends MWException {
+}
diff --git a/includes/upload/exception/UploadStashBadPathException.php b/includes/upload/exception/UploadStashBadPathException.php
new file mode 100644 (file)
index 0000000..470a45a
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashBadPathException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashBadPathException extends UploadStashException {
+}
diff --git a/includes/upload/exception/UploadStashException.php b/includes/upload/exception/UploadStashException.php
new file mode 100644 (file)
index 0000000..f089643
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Implements UploadStashException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashException extends MWException implements ILocalizedException {
+       /** @var string|array|MessageSpecifier */
+       protected $messageSpec;
+
+       /**
+        * @param string|array|MessageSpecifier $messageSpec See Message::newFromSpecifier
+        * @param int $code Exception code
+        * @param Exception|Throwable|null $previous The previous exception used for the exception
+        *  chaining.
+        */
+       public function __construct( $messageSpec, $code = 0, $previous = null ) {
+               $this->messageSpec = $messageSpec;
+
+               $msg = $this->getMessageObject()->text();
+               $msg = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $msg );
+               $msg = Sanitizer::stripAllTags( $msg );
+               parent::__construct( $msg, $code, $previous );
+       }
+
+       public function getMessageObject() {
+               return Message::newFromSpecifier( $this->messageSpec );
+       }
+}
diff --git a/includes/upload/exception/UploadStashFileException.php b/includes/upload/exception/UploadStashFileException.php
new file mode 100644 (file)
index 0000000..0c6e9dd
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashFileException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashFileException extends UploadStashException {
+}
diff --git a/includes/upload/exception/UploadStashFileNotFoundException.php b/includes/upload/exception/UploadStashFileNotFoundException.php
new file mode 100644 (file)
index 0000000..a9df7e4
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashFileNotFoundException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashFileNotFoundException extends UploadStashException {
+}
diff --git a/includes/upload/exception/UploadStashNoSuchKeyException.php b/includes/upload/exception/UploadStashNoSuchKeyException.php
new file mode 100644 (file)
index 0000000..43c63e8
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashNoSuchKeyException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashNoSuchKeyException extends UploadStashException {
+}
diff --git a/includes/upload/exception/UploadStashNotLoggedInException.php b/includes/upload/exception/UploadStashNotLoggedInException.php
new file mode 100644 (file)
index 0000000..18a2e17
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashNotLoggedInException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashNotLoggedInException extends UploadStashException {
+}
diff --git a/includes/upload/exception/UploadStashWrongOwnerException.php b/includes/upload/exception/UploadStashWrongOwnerException.php
new file mode 100644 (file)
index 0000000..aaa836c
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashWrongOwnerException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashWrongOwnerException extends UploadStashException {
+}
diff --git a/includes/upload/exception/UploadStashZeroLengthFileException.php b/includes/upload/exception/UploadStashZeroLengthFileException.php
new file mode 100644 (file)
index 0000000..a054d90
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Implements UploadStashZeroLengthFileException
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @ingroup Upload
+ */
+class UploadStashZeroLengthFileException extends UploadStashException {
+}
index f23c8ee..f4e2e48 100644 (file)
@@ -496,7 +496,6 @@ class User implements IDBAccessObject, UserIdentity {
         * @return string
         */
        protected function getCacheKey( WANObjectCache $cache ) {
-               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
 
                return $cache->makeGlobalKey( 'user', 'id', $lbFactory->getLocalDomainID(), $this->mId );
@@ -520,7 +519,7 @@ class User implements IDBAccessObject, UserIdentity {
         * @since 1.25
         */
        protected function loadFromCache() {
-               $cache = ObjectCache::getMainWANInstance();
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                $data = $cache->getWithSetCallback(
                        $this->getCacheKey( $cache ),
                        $cache::TTL_HOUR,
@@ -589,14 +588,15 @@ class User implements IDBAccessObject, UserIdentity {
                $name = self::getCanonicalName( $name, $validate );
                if ( $name === false ) {
                        return false;
-               } else {
-                       // Create unloaded user object
-                       $u = new User;
-                       $u->mName = $name;
-                       $u->mFrom = 'name';
-                       $u->setItemLoaded( 'name' );
-                       return $u;
                }
+
+               // Create unloaded user object
+               $u = new User;
+               $u->mName = $name;
+               $u->mFrom = 'name';
+               $u->setItemLoaded( 'name' );
+
+               return $u;
        }
 
        /**
@@ -1126,12 +1126,12 @@ class User implements IDBAccessObject, UserIdentity {
                }
 
                // Preg yells if you try to give it an empty string
-               if ( $wgInvalidUsernameCharacters !== '' ) {
-                       if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
-                               wfDebugLog( 'username', __METHOD__ .
-                                       ": '$name' invalid due to wgInvalidUsernameCharacters" );
-                               return false;
-                       }
+               if ( $wgInvalidUsernameCharacters !== '' &&
+                       preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name )
+               ) {
+                       wfDebugLog( 'username', __METHOD__ .
+                               ": '$name' invalid due to wgInvalidUsernameCharacters" );
+                       return false;
                }
 
                return self::isUsableName( $name );
@@ -1161,19 +1161,20 @@ class User implements IDBAccessObject, UserIdentity {
                $result = $this->checkPasswordValidity( $password );
                if ( $result->isGood() ) {
                        return true;
-               } else {
-                       $messages = [];
-                       foreach ( $result->getErrorsByType( 'error' ) as $error ) {
-                               $messages[] = $error['message'];
-                       }
-                       foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
-                               $messages[] = $warning['message'];
-                       }
-                       if ( count( $messages ) === 1 ) {
-                               return $messages[0];
-                       }
-                       return $messages;
                }
+
+               $messages = [];
+               foreach ( $result->getErrorsByType( 'error' ) as $error ) {
+                       $messages[] = $error['message'];
+               }
+               foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
+                       $messages[] = $warning['message'];
+               }
+               if ( count( $messages ) === 1 ) {
+                       return $messages[0];
+               }
+
+               return $messages;
        }
 
        /**
@@ -1214,12 +1215,14 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $result === false ) {
                        $status->merge( $upp->checkUserPassword( $this, $password ), true );
                        return $status;
-               } elseif ( $result === true ) {
+               }
+
+               if ( $result === true ) {
                        return $status;
-               } else {
-                       $status->error( $result );
-                       return $status; // the isValidPassword hook set a string $result and returned true
                }
+
+               $status->error( $result );
+               return $status; // the isValidPassword hook set a string $result and returned true
        }
 
        /**
@@ -1465,12 +1468,13 @@ class User implements IDBAccessObject, UserIdentity {
                        $this->mGroupMemberships = null; // deferred
                        $this->getEditCount(); // revalidation for nulls
                        return true;
-               } else {
-                       // Invalid user_id
-                       $this->mId = 0;
-                       $this->loadDefaults();
-                       return false;
                }
+
+               // Invalid user_id
+               $this->mId = 0;
+               $this->loadDefaults();
+
+               return false;
        }
 
        /**
@@ -1975,10 +1979,10 @@ class User implements IDBAccessObject, UserIdentity {
                                if ( $blockIsValid && $useBlockCookie ) {
                                        // Use the block.
                                        return $tmpBlock;
-                               } else {
-                                       // If the block is not valid, remove the cookie.
-                                       Block::clearCookie( $this->getRequest()->response() );
                                }
+
+                               // If the block is not valid, remove the cookie.
+                               Block::clearCookie( $this->getRequest()->response() );
                        } else {
                                // If the block doesn't exist, remove the cookie.
                                Block::clearCookie( $this->getRequest()->response() );
@@ -1997,11 +2001,9 @@ class User implements IDBAccessObject, UserIdentity {
        public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
                global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
 
-               if ( !$wgEnableDnsBlacklist ) {
-                       return false;
-               }
-
-               if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
+               if ( !$wgEnableDnsBlacklist ||
+                       ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
+               ) {
                        return false;
                }
 
@@ -2045,9 +2047,9 @@ class User implements IDBAccessObject, UserIdentity {
                                        wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $basename!" );
                                        $found = true;
                                        break;
-                               } else {
-                                       wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
                                }
+
+                               wfDebugLog( 'dnsblacklist', "Requested $host, not found in $basename." );
                        }
                }
 
@@ -2167,11 +2169,9 @@ class User implements IDBAccessObject, UserIdentity {
                        if ( isset( $limits['anon'] ) ) {
                                $keys[$cache->makeKey( 'limiter', $action, 'anon' )] = $limits['anon'];
                        }
-               } else {
+               } elseif ( isset( $limits['user'] ) ) {
                        // limits for logged-in users
-                       if ( isset( $limits['user'] ) ) {
-                               $userLimit = $limits['user'];
-                       }
+                       $userLimit = $limits['user'];
                }
 
                // limits for anons and for newbie logged-in users
@@ -2461,7 +2461,9 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $this->mId === null && $this->mName !== null && self::isIP( $this->mName ) ) {
                        // Special case, we know the user is anonymous
                        return 0;
-               } elseif ( !$this->isItemLoaded( 'id' ) ) {
+               }
+
+               if ( !$this->isItemLoaded( 'id' ) ) {
                        // Don't load if this was initialized from an ID
                        $this->load();
                }
@@ -2486,14 +2488,15 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $this->isItemLoaded( 'name', 'only' ) ) {
                        // Special case optimisation
                        return $this->mName;
-               } else {
-                       $this->load();
-                       if ( $this->mName === false ) {
-                               // Clean up IPs
-                               $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
-                       }
-                       return $this->mName;
                }
+
+               $this->load();
+               if ( $this->mName === false ) {
+                       // Clean up IPs
+                       $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
+               }
+
+               return $this->mName;
        }
 
        /**
@@ -2639,7 +2642,9 @@ class User implements IDBAccessObject, UserIdentity {
                $talks = [];
                if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
                        return $talks;
-               } elseif ( !$this->getNewtalk() ) {
+               }
+
+               if ( !$this->getNewtalk() ) {
                        return [];
                }
                $utp = $this->getTalkPage();
@@ -2652,7 +2657,7 @@ class User implements IDBAccessObject, UserIdentity {
                $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
                return [
                        [
-                               'wiki' => WikiMap::getWikiIdFromDomain( WikiMap::getCurrentWikiDomain() ),
+                               'wiki' => WikiMap::getWikiIdFromDbDomain( WikiMap::getCurrentWikiDbDomain() ),
                                'link' => $utp->getLocalURL(),
                                'rev' => $rev
                        ]
@@ -2667,19 +2672,19 @@ class User implements IDBAccessObject, UserIdentity {
        public function getNewMessageRevisionId() {
                $newMessageRevisionId = null;
                $newMessageLinks = $this->getNewMessageLinks();
-               if ( $newMessageLinks ) {
-                       // Note: getNewMessageLinks() never returns more than a single link
-                       // and it is always for the same wiki, but we double-check here in
-                       // case that changes some time in the future.
-                       if ( count( $newMessageLinks ) === 1
-                               && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] )
-                               && $newMessageLinks[0]['rev']
-                       ) {
-                               /** @var Revision $newMessageRevision */
-                               $newMessageRevision = $newMessageLinks[0]['rev'];
-                               $newMessageRevisionId = $newMessageRevision->getId();
-                       }
+
+               // Note: getNewMessageLinks() never returns more than a single link
+               // and it is always for the same wiki, but we double-check here in
+               // case that changes some time in the future.
+               if ( $newMessageLinks && count( $newMessageLinks ) === 1
+                       && WikiMap::isCurrentWikiId( $newMessageLinks[0]['wiki'] )
+                       && $newMessageLinks[0]['rev']
+               ) {
+                       /** @var Revision $newMessageRevision */
+                       $newMessageRevision = $newMessageLinks[0]['rev'];
+                       $newMessageRevisionId = $newMessageRevision->getId();
                }
+
                return $newMessageRevisionId;
        }
 
@@ -2719,10 +2724,10 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $dbw->affectedRows() ) {
                        wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
                        return true;
-               } else {
-                       wfDebug( __METHOD__ . " already set ($field, $id)\n" );
-                       return false;
                }
+
+               wfDebug( __METHOD__ . " already set ($field, $id)\n" );
+               return false;
        }
 
        /**
@@ -2739,10 +2744,10 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $dbw->affectedRows() ) {
                        wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
                        return true;
-               } else {
-                       wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
-                       return false;
                }
+
+               wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
+               return false;
        }
 
        /**
@@ -2809,7 +2814,7 @@ class User implements IDBAccessObject, UserIdentity {
                        return;
                }
 
-               $cache = ObjectCache::getMainWANInstance();
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
                $key = $this->getCacheKey( $cache );
                if ( $mode === 'refresh' ) {
                        $cache->delete( $key, 1 );
@@ -3023,25 +3028,30 @@ class User implements IDBAccessObject, UserIdentity {
                if ( !$this->mToken ) {
                        // The user doesn't have a token, return null to indicate that.
                        return null;
-               } elseif ( $this->mToken === self::INVALID_TOKEN ) {
+               }
+
+               if ( $this->mToken === self::INVALID_TOKEN ) {
                        // We return a random value here so existing token checks are very
                        // likely to fail.
                        return MWCryptRand::generateHex( self::TOKEN_LENGTH );
-               } elseif ( $wgAuthenticationTokenVersion === null ) {
+               }
+
+               if ( $wgAuthenticationTokenVersion === null ) {
                        // $wgAuthenticationTokenVersion not in use, so return the raw secret
                        return $this->mToken;
-               } else {
-                       // $wgAuthenticationTokenVersion in use, so hmac it.
-                       $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
-
-                       // The raw hash can be overly long. Shorten it up.
-                       $len = max( 32, self::TOKEN_LENGTH );
-                       if ( strlen( $ret ) < $len ) {
-                               // Should never happen, even md5 is 128 bits
-                               throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
-                       }
-                       return substr( $ret, -$len );
                }
+
+               // $wgAuthenticationTokenVersion in use, so hmac it.
+               $ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
+
+               // The raw hash can be overly long. Shorten it up.
+               $len = max( 32, self::TOKEN_LENGTH );
+               if ( strlen( $ret ) < $len ) {
+                       // Should never happen, even md5 is 128 bits
+                       throw new \UnexpectedValueException( 'Hmac returned less than 128 bits' );
+               }
+
+               return substr( $ret, -$len );
        }
 
        /**
@@ -3130,19 +3140,17 @@ class User implements IDBAccessObject, UserIdentity {
                $type = $oldaddr != '' ? 'changed' : 'set';
                $notificationResult = null;
 
-               if ( $wgEmailAuthentication ) {
+               if ( $wgEmailAuthentication && $type === 'changed' ) {
                        // Send the user an email notifying the user of the change in registered
                        // email address on their previous email address
-                       if ( $type == 'changed' ) {
-                               $change = $str != '' ? 'changed' : 'removed';
-                               $notificationResult = $this->sendMail(
-                                       wfMessage( 'notificationemail_subject_' . $change )->text(),
-                                       wfMessage( 'notificationemail_body_' . $change,
-                                               $this->getRequest()->getIP(),
-                                               $this->getName(),
-                                               $str )->text()
-                               );
-                       }
+                       $change = $str != '' ? 'changed' : 'removed';
+                       $notificationResult = $this->sendMail(
+                               wfMessage( 'notificationemail_subject_' . $change )->text(),
+                               wfMessage( 'notificationemail_body_' . $change,
+                                       $this->getRequest()->getIP(),
+                                       $this->getName(),
+                                       $str )->text()
+                       );
                }
 
                $this->setEmail( $str );
@@ -3212,9 +3220,9 @@ class User implements IDBAccessObject, UserIdentity {
 
                if ( array_key_exists( $oname, $this->mOptions ) ) {
                        return $this->mOptions[$oname];
-               } else {
-                       return $defaultOverride;
                }
+
+               return $defaultOverride;
        }
 
        /**
@@ -3544,14 +3552,15 @@ class User implements IDBAccessObject, UserIdentity {
                global $wgSecureLogin;
                if ( !$wgSecureLogin ) {
                        return false;
-               } else {
-                       $https = $this->getBoolOption( 'prefershttps' );
-                       Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
-                       if ( $https ) {
-                               $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
-                       }
-                       return $https;
                }
+
+               $https = $this->getBoolOption( 'prefershttps' );
+               Hooks::run( 'UserRequiresHTTPS', [ $this, &$https ] );
+               if ( $https ) {
+                       $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
+               }
+
+               return $https;
        }
 
        /**
@@ -3933,10 +3942,10 @@ class User implements IDBAccessObject, UserIdentity {
        public function getRequest() {
                if ( $this->mRequest ) {
                        return $this->mRequest;
-               } else {
-                       global $wgRequest;
-                       return $wgRequest;
                }
+
+               global $wgRequest;
+               return $wgRequest;
        }
 
        /**
@@ -4108,19 +4117,18 @@ class User implements IDBAccessObject, UserIdentity {
                $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
                $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
 
-               if (
-                       $editCount < $wgLearnerEdits ||
-                       $registration > $learnerRegistration
-               ) {
+               if ( $editCount < $wgLearnerEdits ||
+               $registration > $learnerRegistration ) {
                        return 'newcomer';
-               } elseif (
-                       $editCount > $wgExperiencedUserEdits &&
+               }
+
+               if ( $editCount > $wgExperiencedUserEdits &&
                        $registration <= $experiencedRegistration
                ) {
                        return 'experienced';
-               } else {
-                       return 'learner';
                }
+
+               return 'learner';
        }
 
        /**
@@ -4937,9 +4945,9 @@ class User implements IDBAccessObject, UserIdentity {
                                return false;
                        }
                        return true;
-               } else {
-                       return $confirmed;
                }
+
+               return $confirmed;
        }
 
        /**
@@ -5116,31 +5124,6 @@ class User implements IDBAccessObject, UserIdentity {
                return true;
        }
 
-       /**
-        * Get the localized descriptive name for a group, if it exists
-        * @deprecated since 1.29 Use UserGroupMembership::getGroupName instead
-        *
-        * @param string $group Internal group name
-        * @return string Localized descriptive group name
-        */
-       public static function getGroupName( $group ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               return UserGroupMembership::getGroupName( $group );
-       }
-
-       /**
-        * Get the localized descriptive name for a member of a group, if it exists
-        * @deprecated since 1.29 Use UserGroupMembership::getGroupMemberName instead
-        *
-        * @param string $group Internal group name
-        * @param string $username Username for gender (since 1.19)
-        * @return string Localized name for group member
-        */
-       public static function getGroupMember( $group, $username = '#' ) {
-               wfDeprecated( __METHOD__, '1.29' );
-               return UserGroupMembership::getGroupMemberName( $group, $username );
-       }
-
        /**
         * Return the set of defined explicit groups.
         * The implicit groups (by default *, 'user' and 'autoconfirmed')
@@ -5215,9 +5198,9 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $title ) {
                        return MediaWikiServices::getInstance()
                                ->getLinkRenderer()->makeLink( $title, $text );
-               } else {
-                       return htmlspecialchars( $text );
                }
+
+               return htmlspecialchars( $text );
        }
 
        /**
@@ -5240,9 +5223,9 @@ class User implements IDBAccessObject, UserIdentity {
                if ( $title ) {
                        $page = $title->getFullText();
                        return "[[$page|$text]]";
-               } else {
-                       return $text;
                }
+
+               return $text;
        }
 
        /**
@@ -5711,9 +5694,9 @@ class User implements IDBAccessObject, UserIdentity {
 
                if ( $groups ) {
                        return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
-               } else {
-                       return Status::newFatal( 'badaccess-group0' );
                }
+
+               return Status::newFatal( 'badaccess-group0' );
        }
 
        /**
index f4e3af2..2801207 100644 (file)
@@ -32,6 +32,11 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
         */
        private $actualStore;
 
+       /**
+        * @var string
+        */
+       const DB_READONLY_ERROR = 'The watchlist is currently readonly.';
+
        /**
         * Initialy set WatchedItemStore that will be used in cases where writing is not needed.
         * @param WatchedItemStoreInterface $actualStore
@@ -91,23 +96,23 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
        }
 
        public function duplicateAllAssociatedEntries( LinkTarget $oldTarget, LinkTarget $newTarget ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function duplicateEntry( LinkTarget $oldTarget, LinkTarget $newTarget ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function addWatch( User $user, LinkTarget $target ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function addWatchBatchForUser( User $user, array $targets ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function removeWatch( User $user, LinkTarget $target ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function setNotificationTimestampsForUser(
@@ -115,15 +120,15 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
                $timestamp,
                array $targets = []
        ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function updateNotificationTimestamp( User $editor, LinkTarget $target, $timestamp ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function resetAllNotificationTimestampsForUser( User $user ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function resetNotificationTimestamp(
@@ -132,19 +137,19 @@ class NoWriteWatchedItemStore implements WatchedItemStoreInterface {
                $force = '',
                $oldid = 0
        ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function clearUserWatchedItems( User $user ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function clearUserWatchedItemsUsingJobQueue( User $user ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
        public function removeWatchBatchForUser( User $user, array $titles ) {
-               throw new DBReadOnlyError( null, 'The watchlist is currently readonly.' );
+               throw new DBReadOnlyError( null, self::DB_READONLY_ERROR );
        }
 
 }
index 5f38775..1c33754 100644 (file)
@@ -907,6 +907,10 @@ class WatchedItemStore implements WatchedItemStoreInterface, StatsdAwareInterfac
                        return false;
                }
 
+               if ( !Hooks::run( 'BeforeResetNotificationTimestamp', [ &$user, &$title, $force, &$oldid ] ) ) {
+                       return false;
+               }
+
                $item = null;
                if ( $force != 'force' ) {
                        $item = $this->loadWatchedItem( $user, $title );
index 0531c1f..3dbde01 100644 (file)
@@ -76,9 +76,9 @@ class Language {
        /**
         * @var LocalisationCache
         */
-       static public $dataCache;
+       public static $dataCache;
 
-       static public $mLangObjCache = [];
+       public static $mLangObjCache = [];
 
        /**
         * Return a fallback chain for messages in getFallbacksFor
@@ -92,38 +92,38 @@ class Language {
         */
        const STRICT_FALLBACKS = 1;
 
-       static public $mWeekdayMsgs = [
+       public static $mWeekdayMsgs = [
                'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
                'friday', 'saturday'
        ];
 
-       static public $mWeekdayAbbrevMsgs = [
+       public static $mWeekdayAbbrevMsgs = [
                'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
        ];
 
-       static public $mMonthMsgs = [
+       public static $mMonthMsgs = [
                'january', 'february', 'march', 'april', 'may_long', 'june',
                'july', 'august', 'september', 'october', 'november',
                'december'
        ];
-       static public $mMonthGenMsgs = [
+       public static $mMonthGenMsgs = [
                'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
                'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
                'december-gen'
        ];
-       static public $mMonthAbbrevMsgs = [
+       public static $mMonthAbbrevMsgs = [
                'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
                'sep', 'oct', 'nov', 'dec'
        ];
 
-       static public $mIranianCalendarMonthMsgs = [
+       public static $mIranianCalendarMonthMsgs = [
                'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
                'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
                'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
                'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
        ];
 
-       static public $mHebrewCalendarMonthMsgs = [
+       public static $mHebrewCalendarMonthMsgs = [
                'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
                'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
                'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
@@ -131,7 +131,7 @@ class Language {
                'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
        ];
 
-       static public $mHebrewCalendarMonthGenMsgs = [
+       public static $mHebrewCalendarMonthGenMsgs = [
                'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
                'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
                'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
@@ -139,7 +139,7 @@ class Language {
                'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
        ];
 
-       static public $mHijriCalendarMonthMsgs = [
+       public static $mHijriCalendarMonthMsgs = [
                'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
                'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
                'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
@@ -150,7 +150,7 @@ class Language {
         * @since 1.20
         * @var array
         */
-       static public $durationIntervals = [
+       public static $durationIntervals = [
                'millennia' => 31556952000,
                'centuries' => 3155695200,
                'decades' => 315569520,
@@ -168,26 +168,26 @@ class Language {
         * @since 1.21
         * @var array
         */
-       static private $fallbackLanguageCache = [];
+       private static $fallbackLanguageCache = [];
 
        /**
         * Cache for grammar rules data
         * @var MapCacheLRU|null
         */
-       static private $grammarTransformations;
+       private static $grammarTransformations;
 
        /**
         * Cache for language names
         * @var HashBagOStuff|null
         */
-       static private $languageNameCache;
+       private static $languageNameCache;
 
        /**
         * Unicode directional formatting characters, for embedBidi()
         */
-       static private $lre = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
-       static private $rle = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
-       static private $pdf = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
+       private static $lre = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
+       private static $rle = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
+       private static $pdf = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
 
        /**
         * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
@@ -202,7 +202,7 @@ class Language {
         */
        // @codeCoverageIgnoreStart
        // phpcs:ignore Generic.Files.LineLength
-       static private $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
+       private static $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
        // @codeCoverageIgnoreEnd
 
        /**
@@ -507,7 +507,7 @@ class Language {
         * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
         * available in localised form, it will be included in English.
         *
-        * @return array
+        * @return string[] List of localized namespace names, indexed by numeric namespace ID.
         */
        public function getNamespaces() {
                if ( is_null( $this->namespaceNames ) ) {
@@ -4643,7 +4643,7 @@ class Language {
 
        /**
         * @param string $talk
-        * @return mixed
+        * @return string
         */
        function fixVariableInNamespace( $talk ) {
                if ( strpos( $talk, '$1' ) === false ) {
index 8fdf4f5..006e3b7 100644 (file)
@@ -37,7 +37,7 @@ class LanguageConverter {
         * @since 1.20
         * @var array
         */
-       static public $languagesWithVariants = [
+       public static $languagesWithVariants = [
                'en',
                'crh',
                'gan',
index bd90fc6..ada9f7d 100644 (file)
@@ -63,7 +63,7 @@ class CrhExceptions {
        private function addMappings( $mapArray, &$A2B, &$B2A, $exactCase = false,
                        $prePat = '', $postPat = '' ) {
                foreach ( $mapArray as $WordA => $WordB ) {
-                       if ( ! $exactCase ) {
+                       if ( !$exactCase ) {
                                $ucA = $this->myUc( $WordA );
                                $ucWordA = $this->myUcWord( $WordA );
                                $ucB = $this->myUc( $WordB );
@@ -71,17 +71,17 @@ class CrhExceptions {
                        }
 
                        # if there are regexes, only map toward backregs
-                       if ( ! preg_match( '/\$[1-9]/', $WordA ) ) {
+                       if ( !preg_match( '/\$[1-9]/', $WordA ) ) {
                                $A2B[ $prePat . $WordA . $postPat ] = $WordB;
-                               if ( ! $exactCase ) {
+                               if ( !$exactCase ) {
                                        $A2B[ $prePat . $ucWordA . $postPat ] = $ucWordB;
                                        $A2B[ $prePat . $ucA . $postPat ] = $ucB;
                                }
                        }
 
-                       if ( ! preg_match( '/\$[1-9]/', $WordB ) ) {
+                       if ( !preg_match( '/\$[1-9]/', $WordB ) ) {
                                $B2A[ $prePat . $WordB . $postPat ] = $WordA;
-                               if ( ! $exactCase ) {
+                               if ( !$exactCase ) {
                                        $B2A[ $prePat . $ucWordB . $postPat ] = $ucWordA;
                                        $B2A[ $prePat . $ucB . $postPat ] = $ucA;
                                }
index e7b441c..37055b1 100644 (file)
        "subject": "موضوع/عنوان:",
        "minoredit": "هذا تعديل طفيف",
        "watchthis": "راقب هذه الصفحة",
-       "savearticle": "احÙ\81ظ Ø§Ù\84صÙ\81حة",
+       "savearticle": "حفظ الصفحة",
        "preview": "معاينة",
        "showpreview": "أظهر معاينة",
        "showdiff": "ورّي التبديلات",
        "tooltip-ca-nstab-template": "رؤية القالب",
        "tooltip-ca-nstab-category": "رؤية صفحة التصنيف",
        "tooltip-minoredit": "علم على هذا كتعديل طفيف",
-       "tooltip-save": "احÙ\81ظ ØªØºÙ\8aÙ\8aراتÙ\83",
+       "tooltip-save": "حفظ تغييراتك",
        "tooltip-preview": "اعرض تغييراتك، من فضلك استخدم هذا قبل الحفظ!",
        "tooltip-diff": "اعرض التغييرات التي قمت بها للنص.",
        "tooltip-compareselectedversions": "شاهد الفروق بين النسختين المختارتين من هذه الصفحة.",
index 0f8f7b7..91191b7 100644 (file)
        "returnto": "Gān eft tō $1",
        "tagline": "Fram {{SITENAME}}",
        "help": "Help",
+       "help-mediawiki": "Help ymbe MediaWiki",
        "search": "Sēcan",
        "searchbutton": "Sēcan",
        "go": "Gān",
        "badarticleerror": "Þēos dǣd ne cann bēon gefremed on þissum tramete.",
        "cannotdelete": "Se tramet oþðe ymele \"$1\" ne meahte beon ahwiten. Meahtlice hæfþ adihtere ær hine astricon.",
        "cannotdelete-title": "Ne cann forlēosan þone tramet \"$1\"",
+       "delete-hook-aborted": "Ahwitung aswamode of hacan. He geaf nan racu.",
        "badtitle": "Nā genge titul",
        "title-invalid-too-long": "Se trametnama þone þū wilt is to lang.  He sceal bēon ne langor þon $1 {{PLURAL:$1|byte|bytan}} in UTF-8 rūncræfte.",
        "title-invalid-leading-colon": "Se trametnama þone þū wilt beclypeþ unregolfæsten twafealden prican æt his orde.",
        "myprivateinfoprotected": "Þū nafast lēafe tō adihtenne þīne āgnan cȳþþu.",
        "mypreferencesprotected": "Þū nafast lēafe tō adihtenne þīna foreberunga.",
        "ns-specialprotected": "Syndrige trametas ne cunnon wesan adihted.",
+       "invalidtitle": "Ungenga titul",
+       "invalidtitle-knownnamespace": "Ungenga titul mid namafæce \"S=$2\" and gewrite \"$3\"",
+       "invalidtitle-unknownnamespace": "Ungenga titul mid uncuþe namafæce rim $1 and gewrite \"$2\"",
        "exception-nologin": "Ne inloggod",
        "virus-badscanner": "Yfel gesetedness: Uncūþ wyrmsēcend: <em>$1</em>",
        "virus-unknownscanner": "uncūþ andgund:",
index 24653d2..e79de32 100644 (file)
        "subject": "موضوع/عنوان:",
        "minoredit": "هذا تعديل طفيف",
        "watchthis": "راقب هذه الصفحة",
-       "savearticle": "احÙ\81ظ Ø§Ù\84صÙ\81حة",
-       "savechanges": "احÙ\81ظ Ø§Ù\84تغÙ\8aÙ\8aرات",
+       "savearticle": "حفظ الصفحة",
+       "savechanges": "حفظ التغييرات",
        "publishpage": "نشر الصفحة",
        "publishchanges": "نشر التغييرات",
-       "savearticle-start": "احÙ\81ظ Ø§Ù\84صÙ\81حةâ\80¦",
+       "savearticle-start": "حفظ الصفحة…",
        "savechanges-start": "حفظ التغييرات...",
        "publishpage-start": "نشر الصفحة...",
        "publishchanges-start": "نشر التغييرات...",
        "editingsection": "تعديل $1 (قسم)",
        "editingcomment": "إنشاء قسم من «$1»",
        "editconflict": "تضارب تحرير: $1",
-       "explainconflict": "لقد عدل شخص آخر هذه الصفحة بعد أن بدأت أنت بتحريرها.\nصندوق النصوص العلوي يحتوي على النص الموجود حاليا في الصفحة.\nالتعديلات التي أجريتها أنت معروضة في الصندوق أسفله.\nويلزم دمجها في النص الموجود حاليا.\n'''لن يحفظ''' بعد الضغط على زر \"احفظ الصفحة\" '''إلا''' ما هو موجود في الصندوق العلوي.",
+       "explainconflict": "لقد عدل شخص آخر هذه الصفحة بعد أن بدأت أنت بتحريرها.\nصندوق النصوص العلوي يحتوي على النص الموجود حاليا في الصفحة.\nالتعديلات التي أجريتها أنت معروضة في الصندوق أسفله.\nويلزم دمجها في النص الموجود حاليا.\n<strong>لن يحفظ</strong> بعد الضغط على زر \"$1\" إلا ما هو موجود في الصندوق العلوي.",
        "yourtext": "نصك",
        "storedversion": "النسخة المخزنة",
        "editingold": "''' تحذير: أنت تقوم الآن بتحرير نسخة قديمة من هذه الصفحة.\nإذا قمت بحفظها، ستفقد كافة التغييرات التي حدثت بعد هذه النسخة. '''",
        "prefs-setemail": "تعيين عنوان البريد الإلكتروني",
        "prefs-email": "خيارات البريد الإلكتروني",
        "prefs-rendering": "المظهر",
-       "saveprefs": "احÙ\81ظ",
+       "saveprefs": "حفظ",
        "restoreprefs": "استرجاع كل الإعدادات الافتراضية (في كل الأقسام)",
        "prefs-editing": "التحرير",
        "searchresultshead": "بحث",
        "viewinguserrights": "عرض صلاحيات المستخدم {{GENDER:$1|للمستخدم|للمستخدمة}} <strong>[[User:$1|$1]]</strong> $2",
        "userrights-editusergroup": "تعديل مجموعات {{GENDER:$1|المستخدم|المستخدمة}}",
        "userrights-viewusergroup": "عرض مجموعات {{GENDER:$1|المستخدم|المستخدمة}}",
-       "saveusergroups": "احÙ\81ظ Ù\85جÙ\85Ù\88عات {{GENDER:$1|اÙ\84Ù\85ستخدÙ\85|اÙ\84Ù\85ستخدÙ\85Ø©}}",
+       "saveusergroups": "حفظ مجموعات {{GENDER:$1|المستخدم|المستخدمة}}",
        "userrights-groupsmember": "عضو في:",
        "userrights-groupsmember-auto": "عضو ضمني في:",
        "userrights-groups-help": "يمكنك تغيير المجموعات التي ينتمي هذا المستخدم إليها:\n* يعني الصندوق المعلم أن المستخدم ضمن هذه المجموعة.\n* يعني الصندوق غير المعلم أن المستخدم ليس في هذه المجموعة.\n* تعني علامة * عدم إمكانية إزالة المجموعة متى ما أضفتها أو العكس.\n* تعني علامة # أنه يمكنك فقط تحديد تاريخ الانتهاء لعضوية هذه المجموعة؛ لكن لا يمكنك تقديمه بعد تحديده.",
        "rcfilters-savedqueries-apply-label": "أنشئ مرشحا",
        "rcfilters-savedqueries-apply-and-setdefault-label": "إنشاء مرشح افتراضي",
        "rcfilters-savedqueries-cancel-label": "ألغ",
-       "rcfilters-savedqueries-add-new-title": "احÙ\81ظ Ø¥Ø¹Ø¯Ø§Ø¯Ø§Øª Ø§Ù\84Ù\85رشحات الحالية",
+       "rcfilters-savedqueries-add-new-title": "Ø­Ù\81ظ Ø¥Ø¹Ø¯Ø§Ø¯Ø§Øª Ø§Ù\84Ù\85رشح الحالية",
        "rcfilters-savedqueries-already-saved": "هذه المرشحات محفوظة بالفعل. غير إعداداتك لإنشاء مرشح محفوظ جديد.",
        "rcfilters-restore-default-filters": "استرجاع المرشحات الافتراضية",
        "rcfilters-clear-all-filters": "مسح كل المرشحات",
        "uploadwarning": "تحذير الرفع",
        "uploadwarning-text": "من فضلك عدل وصف الملف أدناه وحاول مرة أخرى.",
        "uploadwarning-text-nostash": "من فضلك أعد رفع الملف، عدل الوصف بالأسفل وحاول مرة ثانية.",
-       "savefile": "احÙ\81ظ Ø§Ù\84Ù\85Ù\84Ù\81",
+       "savefile": "حفظ الملف",
        "uploaddisabled": "تم تعطيل الرفع",
        "copyuploaddisabled": "الرفع من مسار معطل.",
        "uploaddisabledtext": "رفع الملفات معطل.",
        "upload-dialog-button-cancel": "إلغاء",
        "upload-dialog-button-back": "رجوع",
        "upload-dialog-button-done": "تم",
-       "upload-dialog-button-save": "احÙ\81ظ",
+       "upload-dialog-button-save": "حفظ",
        "upload-dialog-button-upload": "رفع",
        "upload-form-label-infoform-title": "التفاصيل",
        "upload-form-label-infoform-name": "الاسم",
        "export-addcat": "أضف",
        "export-addnstext": "أضف صفحات من نطاق:",
        "export-addns": "أضف",
-       "export-download": "احÙ\81ظ Ù\83Ù\85Ù\84Ù\81",
+       "export-download": "حفظ كملف",
        "export-templates": "ضمن القوالب",
        "export-pagelinks": "ضمن الصفحات الموصولة إلى عمق:",
        "export-manual": "إضافة صفحات يدويا:",
        "tooltip-ca-nstab-help": "رؤية صفحة المساعدة",
        "tooltip-ca-nstab-category": "رؤية صفحة التصنيف",
        "tooltip-minoredit": "علم على هذا كتعديل طفيف",
-       "tooltip-save": "احÙ\81ظ ØªØºÙ\8aÙ\8aراتÙ\83",
+       "tooltip-save": "حفظ تغييراتك",
        "tooltip-publish": "انشر تغييراتك",
        "tooltip-preview": "اعرض تغييراتك، من فضلك استخدم هذا قبل الحفظ!",
        "tooltip-diff": "اعرض التغييرات التي أجريتها على النص.",
        "tooltip-upload": "ابدأ الرفع",
        "tooltip-rollback": "\"استرجاع\" تسترجع تعديلات آخر محرر لهذه الصفحة بضغطة واحدة",
        "tooltip-undo": "\"رجوع\" تسترجع هذا التعديل وتفتح نافذة التعديل في نمط العرض المسبق. تسمح بإضافة سبب في الملخص.",
-       "tooltip-preferences-save": "احÙ\81ظ Ø§Ù\84تغÙ\8aÙ\8aرات",
+       "tooltip-preferences-save": "حفظ التغييرات",
        "tooltip-summary": "أدخل ملخصا قصيرا",
-       "common.css": "/* Ø³ØªØ¤Ø«Ø± Ø§Ù\84Ø£Ù\86Ù\85اط Ø§Ù\84Ù\85تراصة (CSS) Ø§Ù\84Ù\85عرÙ\88ضة Ù\87Ù\86ا على كل الواجهات */",
+       "common.css": "/* Ø§Ù\84Ø£Ù\86Ù\85اط Ø§Ù\84Ù\85تراصة CSS Ø§Ù\84Ù\85عرÙ\88ضة Ù\87Ù\86ا Ø³ØªØ¤Ø«Ø± على كل الواجهات */",
        "print.css": "/* الأنماط المتراصة CSS المعروضة هنا ستؤثر على ناتج الطباعة */",
        "noscript.css": "/* الأنماط المتراصة CSS المعروضة هنا ستؤثر على المستخدمين الذين الجافاسكريبت لديهم معطلة */",
        "group-autoconfirmed.css": "/* الأنماط المتراصة CSS المعروضة هنا ستؤثر على المستخدمين المؤكدين تلقائيا فقط */",
        "group-sysop.css": "/* الأنماط المتراصة CSS المعروضة هنا ستؤثر على الإداريين فقط */",
        "group-bureaucrat.css": "/* الأنماط المتراصة CSS المعروضة هنا ستؤثر على البيروقراطيين فقط */",
        "common.json": "/* سيتم تحميل أي جسون هنا لجميع المستخدمين في كل تحميل للصفحة. */",
-       "common.js": "/* Ø§Ù\84جاÙ\81اسÙ\83رÙ\8aبت Ø§Ù\84Ù\85Ù\88ضÙ\88ع Ù\87Ù\86ا Ø³Ù\8aتÙ\85 ØªØ­Ù\85Ù\8aÙ\84Ù\87 لكل المستخدمين مع كل تحميل للصفحة. */",
+       "common.js": "/* Ø£Ù\8a Ø¬Ø§Ù\81اسÙ\83رÙ\8aبت Ù\87Ù\86ا Ø³Ù\8aتÙ\85 ØªØ­Ù\85Ù\8aÙ\84Ù\87ا لكل المستخدمين مع كل تحميل للصفحة. */",
        "group-autoconfirmed.js": "/* أي جافاسكريبت هنا سيتم تحميلها للمستخدمين المؤكدين تلقائيا فقط */",
        "group-user.js": "/* أي JavaScript هنا سيتم تحميله للمستخدمين المسجلين فقط */",
        "group-bot.js": "/* أي جافاسكريبت هنا سيتم تحميلها للبوتات فقط */",
        "hebrew-calendar-m6-gen": "أدار",
        "hebrew-calendar-m6a-gen": "أدار الأول",
        "hebrew-calendar-m6b-gen": "أدار الثاني",
-       "hebrew-calendar-m7-gen": "نيسان/أبريل",
+       "hebrew-calendar-m7-gen": "نيسان",
        "hebrew-calendar-m8-gen": "أيار",
        "hebrew-calendar-m9-gen": "سيفان",
        "hebrew-calendar-m10-gen": "تموز",
        "mediastatistics-header-text": "نصي",
        "mediastatistics-header-executable": "تنفيذية",
        "mediastatistics-header-archive": "صيغ مضغوطة",
+       "mediastatistics-header-3d": "ثلاثي الأبعاد",
        "mediastatistics-header-total": "كل الملفات",
        "json-warn-trailing-comma": "تمت إزالة {{PLURAL:$1|فاصلة انتهائية واحدة|فاصلتين انتهائيتين|$1 فاصلات انتهائية|$1 فاصلة انتهائية}} من JSON",
        "json-error-unknown": "وقعت مشكلة مع JSON. رسالة الخطأ: $1",
index 85d1118..4616ac3 100644 (file)
        "parser-template-loop-warning": "لووب القالب المحدد: [[$1]]",
        "parser-template-recursion-depth-warning": "حد عمق الريكيرشيون بتاع القالب اتعدى  ($1)",
        "language-converter-depth-warning": "حد عمق محول اللغه اتعدى ($1)",
-       "undo-success": "ممكن ترجع فى التعديل.\nلو سمحت تشوف المقارنة اللى تحت علشان تتأكد من إن هو دا اللى إنت عايز تعمله ،وبعدين احفظ التغييرات اللى تحت علشان ترجع فى التعديل.",
+       "undo-success": "ممكن ترجع فى التعديل.\nلو سمحت تشوف المقارنة اللى تحت علشان تتأكد من إن هو دا اللى إنت عايز تعمله، وبعدين سييف التغييرات اللى تحت علشان ترجع فى التعديل.",
        "undo-failure": "الرجوع فى التعديل ما نفعش علشان فى تعديلات متعاكسة حصلت فى الصفحة.",
        "undo-norev": "الرجوع فى التعديل ما نفعش علشان هو يا إما مش موجود أو انه إتمسح.",
        "undo-summary": "الرجوع فى التعديل $1 بتاع [[Special:Contributions/$2|$2]] ([[User talk:$2|نقاش]])",
        "filereuploadsummary": "تغييرات الملف:",
        "filestatus": "حالة حقوق النسخ:",
        "filesource": "مصدر:",
-       "ignorewarning": "إتجاÙ\87Ù\84 Ø§Ù\84تحذÙ\8aر Ù\88 Ø§Ø­Ù\81ظ الملف وخلاص",
+       "ignorewarning": "اتجاÙ\87Ù\84 Ø§Ù\84تحذÙ\8aر Ù\88 Ø³Ù\8aÙ\8aÙ\81 الملف وخلاص",
        "ignorewarnings": "اتجاهل اى تحذير",
        "minlength1": "أسامى الملفات لازم تكون متكونة من حرف واحد على الأقل.",
        "illegalfilename": "اسم الملف \"$1\" فيه علامات  مش مسموح بيها فى عناوين الصفحات.\nلو سمحت تختار اسم تانى للمف و بعدين تحمله من اول و جديد.",
        "export-addcat": "زيادة",
        "export-addnstext": "اضافة صفحات من نطاق:",
        "export-addns": "اضافه",
-       "export-download": "احÙ\81ظ Ù\83Ù\85Ù\84Ù\81",
+       "export-download": "سÙ\8aÙ\8aÙ\81 Ù\83Ù\81اÙ\8aÙ\84",
        "export-templates": "دخل القوالب",
        "export-pagelinks": "تضمين الصفحات المتوصله لحد عمق :",
        "allmessages": "رسايل النظام",
index 732a592..eb2430f 100644 (file)
        "cantcreateaccount-text": "بو ای پی عنوانین‌دان ('$1) ایستیفاده‌چی حسابی یارادیلماسی [[User:$3|$3]] طرفین‌دن انگللنمیش‌دیر.\n\n$3 طرفین‌دن وئریلن سبب '$2",
        "cantcreateaccount-range-text": "<strong>$1</strong> آی‌پی آدرس آرالیغیندان حساب یارانماق، [[User:$3|$3]] ایشلدنی طرفیندن یاساقلانیب‌دیر. سیزین‌ده آی‌پی آدرسیز (<strong>$4</strong>) بو آرادادیر.\n\n$3 طرفین‌دن وئریلن سبب بودور: «$2»",
        "viewpagelogs": "بۇ صفحه‌نین ژورناللارینا باخ",
-       "nohistory": "بو صحیفه اوچون دَییشدیرمه گئچمیشی یوخدور.",
+       "nohistory": "بو صفحه اوچون دَییشدیرمه گئچمیشی یوخدور.",
        "currentrev": "سون نوسخه",
        "currentrev-asof": "$1 تاریخینه کیمی سون حال",
        "revisionasof": "$1 نوسخه‌سی",
        "unusedcategoriestext": "آشاغیدا اولان بولمه لر مؤوجود اولدوغو حالدا، هئچ بیر مقاله یوخسا بولمه لر طرفین‌دن ایشلدیلمیر.",
        "notargettitle": "وئریلمه‌يیب",
        "notargettext": "بو صحیفه یا یا کاربر حیاتا کئچیرمک اوچون بیر هدف صحیفه‌سی یا دا ایستیفاده‌چی‌سی بئلیرتمئدینیز.",
-       "nopagetitle": "بئله هدف صحیفه‌سی یوخ‌دور",
-       "nopagetext": "ایفاده ائتدیگینیز هدف صحیفه‌سی مؤوجود دئییل.",
+       "nopagetitle": "بئله هدف صفحه‌سی یوخدور",
+       "nopagetext": "ایفاده ائتدیگینیز هدف صفحه‌سی مؤوجود دئییل.",
        "pager-newer-n": "{{PLURAL:$1|بیر داها یئنی|$1 داها یئنی}}",
        "pager-older-n": "{{PLURAL:$1|بیر داها کؤهنه|$1 داها کؤهنه}}",
        "suppress": "باخان",
index 7fb3b82..4653868 100644 (file)
        "userrights-expiry": "Тамамлана:",
        "userrights-expiry-existing": "Әлеге ваҡыт сыға:$2, $3",
        "userrights-expiry-othertime": "Башҡа ваҡыт:",
-       "userrights-expiry-options": "1 көн:1 day,1 аҙна:1 week,1 ай:1 mopnth, 3 ай:3 months,6 ай:6 months,1 йыл:1 year",
+       "userrights-expiry-options": "1 көн:1 day,1 аҙна:1 week,1 ай:1 month, 3 ай:3 months,6 ай:6 months,1 йыл:1 year",
        "userrights-invalid-expiry": "«$1» төркөмө өсөн ваҡыт бөтөүе яңылыш бирелгән",
        "userrights-expiry-in-past": "Время истечения для группы «$1» задано в прошлом.\n«$1» төркөмө өсөн ваҡыт бөтөүе үткән ваҡытта бирелгән.",
        "userrights-cannot-shorten-expiry": "«$1» төркөмөндә ағза булыу осоро датаһын алғараҡ күсерә алмайһың. Был төркөмгә өҫтәү йәки алыуға хоҡуғы булған ҡатнашыусылар ғына уны алғараҡ күсерә ала.",
index ca2f5b6..5f03849 100644 (file)
        "brokenredirects-edit": "рэдагаваць",
        "brokenredirects-delete": "выдаліць",
        "withoutinterwiki": "Старонкі без спасылак на іншыя моўныя вэрсіі",
-       "withoutinterwiki-summary": "Наступныя старонкі ня ўтрымліваюць спасылак на іншамоўныя вэрсіі:",
+       "withoutinterwiki-summary": "Наступныя старонкі ня ўтрымліваюць спасылак на іншамоўныя вэрсіі.",
        "withoutinterwiki-legend": "Прэфікс",
        "withoutinterwiki-submit": "Паказаць",
        "fewestrevisions": "Старонкі з найменшай колькасьцю рэдагаваньняў",
        "ipb_expiry_old": "Час сканчэньня ўжо мінуў.",
        "ipb_expiry_temp": "Блякаваньні са схаваньнем імя ўдзельніка павінны быць бестэрміновымі.",
        "ipb_hide_invalid": "Немагчыма схаваць гэты рахунак; зь яго зроблена больш чым {{PLURAL:$1|$1 рэдагаваньне|$1 рэдагаваньні|$1 рэдагаваньняў}}.",
+       "ipb_hide_partial": "Блякаваньні схаваных імёнаў удзельнікаў мусяць пашырацца на ўвесь сайт.",
        "ipb_already_blocked": "«$1» ужо заблякаваны",
        "ipb-needreblock": "$1 ужо заблякаваны. Вы жадаеце зьмяніць парамэтры?",
        "ipb-otherblocks-header": "{{PLURAL:$1|1=Іншае блякаваньне|Іншыя блякаваньні}}",
index a39c990..56b6f0f 100644 (file)
        "customcssprotected": "У вас няма дазволу рэдагаваць гэтую CSS-старонку, бо яна ўтрымлівае асабістыя настройкі іншага ўдзельніка.",
        "customjsonprotected": "У вас няма дазволу рэдагаваць гэтую JSON-старонку, таму што яна ўтрымлівае асабістыя настройкі іншага ўдзельніка.",
        "customjsprotected": "У вас няма дазволу рэдагаваць гэтую JavaScript-старонку, таму што яна ўтрымлівае асабістыя настройкі іншага ўдзельніка.",
-       "sitecssprotected": "У Вас няма правоў на рэдагаванне гэтай JavaScript-старонкі, бо яе змяненне можа паўплываць на ўсіх наведвальнікаў.",
+       "sitecssprotected": "У Вас няма правоў на рэдагаванне гэтай CSS-старонкі, бо яе змяненне можа паўплываць на ўсіх наведвальнікаў.",
        "sitejsonprotected": "У Вас няма правоў на рэдагаванне гэтай JSON-старонкі, бо яе змяненне можа паўплываць на ўсіх наведвальнікаў.",
        "sitejsprotected": "У Вас няма правоў на рэдагаванне гэтай JavaScript-старонкі, бо яе змяненне можа паўплываць на ўсіх наведвальнікаў.",
        "mycustomcssprotected": "Вам не дазволена рэдагаванне гэтай старонкі CSS.",
index 1146436..1c86ba5 100644 (file)
        "subject-preview": "Предварителен преглед на заглавието:",
        "previewerrortext": "Възникна грешка при опита за преглед на промените.",
        "blockedtitle": "Потребителят е блокиран",
+       "blockedtext-partial": "<strong>Вашето потребителско име или IP-адрес е забранено да правите промени на тази страница. Все още можете да редактирате други страници в това уики. </strong> Всички подробности за забраната ще намерите в [[Special:MyContributions|приносите за сметката]].\n\nЗабраната даде $1.\n\nПосочената причина е <em>$2</em>.\n\n* Начало на забраната: $8\n* Край на забраната: $6\n* Забраната се отнася за: $7\n* Идентификатор на забраната #$5",
        "blockedtext": "'''Вашето потребителско име (или IP-адрес) беше блокирано.'''\n\nБлокирането е извършено от $1. Посочената причина е: ''$2''\n\n*Начало на блокирането: $8\n*Край на блокирането: $6\n*Блокирането се отнася за: $7\n\nМожете да се свържете с $1 или с някой от останалите [[{{MediaWiki:Grouppage-sysop}}|администратори]], за да обсъдите блокирането.\n\nМожете да използвате услугата „{{int:emailuser}}“ само ако не ви е забранена употребата ѝ и ако сте посочили валидна електронна поща в [[Special:Preferences|настройките]] си.\n\nВашият IP адрес е $3, а номерът на блокирането е $5. Включвайте едно от двете или и двете във всяко запитване, което правите.",
        "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, а номерът на блокирането ви е $5.\nВключвайте ги във всяка заявка, която правите.",
        "systemblockedtext": "Вашето потребителско име или IP адрес беше автоматично блокирано от Медия Уики.\nПосочената причина е:\n\n:<em>$2</em>\n\n* Начало на блокирането: $8\n* Край на блокирането: $6\n* Блокирането се отнася за: $7\n\nВашият текущ IP адрес е $3.\nМоля, включете всичките детайли по-горе, ако правите каквито и да е запитвания.",
        "ip_range_toolarge": "Забранено е блокиране на диапазони от IP адреси по-големи от /$1.",
        "ip_range_exceeded": "IP диапазонът превишава максималния диапазон. Позволен диапазон: /$1.",
        "proxyblocker": "Блокировач на проксита",
-       "proxyblockreason": "IP-адÑ\80еÑ\81Ñ\8aÑ\82 Ð\92и Ð±ÐµÑ\88е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ан, Ñ\82Ñ\8aй ÐºÐ°Ñ\82о Ðµ Ð°Ð½Ð¾Ð½Ð¸Ð¼Ð½Ð¾ Ð´Ð¾Ñ\81Ñ\82Ñ\8aпен Ð¼ÐµÐ¶Ð´Ð¸Ð½ÐµÐ½ Ñ\81Ñ\8aÑ\80вÑ\8aÑ\80Свържете се с доставчика си на Интернет и го информирайте за този сериозен проблем в сигурността.",
+       "proxyblockreason": "IP-адÑ\80еÑ\81Ñ\8aÑ\82 Ð\92и Ð±ÐµÑ\88е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ан, Ñ\82Ñ\8aй ÐºÐ°Ñ\82о Ð¿Ñ\80едÑ\81Ñ\82авлÑ\8fва Ð°Ð½Ð¾Ð½Ð¸Ð¼Ð½Ð¾ Ð´Ð¾Ñ\81Ñ\82Ñ\8aпен Ð¼ÐµÐ¶Ð´Ð¸Ð½ÐµÐ½ Ñ\81Ñ\8aÑ\80вÑ\8aÑ\80.\nСвържете се с доставчика си на Интернет и го информирайте за този сериозен проблем в сигурността.",
        "sorbs": "DNSBL",
        "sorbsreason": "IP-адресът Ви е записан като анонимно достъпен междинен сървър в DNSBL на {{SITENAME}}.",
        "sorbs_create_account_reason": "IP-адресът Ви е записан като анонимно достъпен междинен сървър в DNSBL на {{SITENAME}}.\nНе може да създадете сметка.",
index 15569c0..a06994f 100644 (file)
        "blocklist-nousertalk": "নিজস্ব আলাপ পাতা সম্পাদনা করতে পারবে না",
        "blocklist-editing": "সম্পাদনা করছেন",
        "blocklist-editing-sitewide": "সম্পাদনা করছেন (সাইটব্যাপী)",
+       "blocklist-editing-page": "পাতাসমূহ",
+       "blocklist-editing-ns": "নামস্থানসমূহ",
        "ipblocklist-empty": "বাধাতালিকা খালি।",
        "ipblocklist-no-results": "অনুরুদ্ধ আইপি ঠিকানা বা ব্যবহারকারী নামটির উপর কোন বাধা নেই।",
        "blocklink": "বাধা দাও",
index 99f87e2..5f32141 100644 (file)
                        "Isevand"
                ]
        },
-       "tog-underline": "هومپیڤٱندا زیر خٱتدار",
+       "tog-underline": "هومپاٛیڤٱندٱل زیر خٱتدار",
        "tog-hideminor": "دٱم تی نٱبیڌن آلشتا کۊچیر",
        "tog-hidepatrolled": "بؽدیار نڤیڌن آلشڌٱل کوچیر",
        "tog-newpageshidepatrolled": "بٱلٛگیٱل لرهٱرڌاْ زاْ فاٛئرست بٱلٛگیٱل نۊ بؽدیار ڤۊهاْ",
        "tog-hidecategorization": "بؽدیارنیڌن رٱئڌاٛڤٱنی بٱلٛگیٱل",
-       "tog-extendwatchlist": "Ú¯Ù¾ Ú©Ø±Ø¯Ù\86 Ù\86Ù\88Ù\85 Ú¯Ù\87 Ø¢ Ù\85Ù\88 Ø³Û\8c Ø¯Û\8cئÙ\86 Ù\87Ù\85Ù\87 Ø¢Ù\84شتا Ù\86Ù\87 Ù\81Ù\82Ø· Ù\87Ù\88Ù\86Ù\88 Ú©Ù\87 Ø¨Û\8cشتر Ø² Ù\87Ù\85Ù\87 Ø§Ù\86جÙ\88Ù\85 Ø§Ø¨Ù\88ن.",
+       "tog-extendwatchlist": "گٱپ Ú©Ø±Ø¯Ù\86 Ù\86Ù\88Ù\85Ú¯Ù±Û\8cÙ±Ù\84 Ù\85Ù\88 Ø³Û\8c Ø¯Û\8cÚ\8cÙ\86 ØªÙ±Ù\85Ù\88Ù\85 Ø¢Ù\84شدکارÛ\8cÙ±Ù\84 Ù\86Ù± Ù\81Ù±Ù\82ٱت Ù\87Ù\88Ù\86Ù\88 Ú©Ø§Ù\92 Ø¨Û\8cشتر Ø² Ù\87Ù±Ù\85اÙ\92 Ù±Ù\86جÙ\88Ù\85 Ø§Ù\92بÛ\8aن.",
        "tog-usenewrc": "جٱرغاٛ کاری آلشتا ڤا آلشتکاری بٱلگاٛیلسۊن و سئیل بٱرگسۊن",
        "tog-numberheadings": "شوماراٛ ڤٱندن خودٱنجوم سی سربٱلگاٛیل",
        "tog-editondblclick": "ڤیرایشت بٱلگاٛیل ڤا دو کئرٱت پۊرنیڌن",
        "tog-editsectiononrightclick": "ڤیرایشت ڤابیڌن ڤا راست پۊرنیڌن ری بٱرجا داسۊن هر جاگٱ",
-       "tog-watchcreations": "اÙ\9bزاÙ\81 Ú©Ø±Ø¯Ù\86 Ø§Ù\88 Ø¨Ù±Ù\84گاÙ\9bÛ\8cÙ\84Û\8c Ú©Ø§Ù\9b Ø®Ù\88Ù\85 Ø±Ø§Ø³Øª Ú©Ø±Ø¯Ù\88Ù\85اÙ\9b Ù\88 Ø§Ù\88 Ø¬Ø§Ù\86Û\8cاÛ\8cÙ\84Û\8c Ú©Ø§Ù\9b Ø®Ù\88Ù\85 Ù\84اÙ\87اÙ\85سÛ\8aÙ\86اÙ\9b Ù\85ئÙ\86 Ø³Ø¦یل بٱرگ خوم",
-       "tog-watchdefault": "اÙ\9bزاÙ\81 Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\9bÛ\8cÙ±Ù\84 Ù\88 Ø¬Ø§Ù\86Û\8cاÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\9b Ù\85Ù\88 Ù\85ئÙ\86 Ø³Ø¦Û\8cÙ\84 Ø¨Ø±Ú¯Ù\88Ù\85 Ú¤Û\8cراÛ\8cشدسÛ\8aÙ\86 Ú©Ø±Ø¯Ù\88Ù\85اÙ\9b",
-       "tog-watchmoves": "اÙ\92زاÙ\81 Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\9bÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\9b Ø®Ù\88Ù\85 Ø¬Ø§ Ø¨ Ø¬Ø§Ø³Û\8aÙ\86 Ú©Ø±Ø¯Ù\88Ù\85اÙ\9b Ø³Û\8c Ø³Ø¦ل بٱرگوم",
-       "tog-watchdeletion": "اÙ\9bزاÙ\81 Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\9bÛ\8cÙ±Ù\84 Ù\88 Ø¬Ø§Ù\86Û\8cاÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\9b Ø®Ù\88Ù\85 Ø² Ù\85ئÙ\86 Ø³Ø¦Û\8cÙ\84 Ø¨Ù±Ø±Ú¯Ù\88Ù\85 Ù¾Ø§Ú©Ø³Ø§ Ú©Ø±Ø¯Ù\88Ù\85اÙ\9b",
+       "tog-watchcreations": "اÙ\9bزاÙ\81 Ú©Ø±Ø¯Ù\86 Ø§Ù\88 Ø¨Ù±Ù\84گاÙ\92Û\8cÙ±Ù\84Û\8c Ú©Ø§Ù\92 Ø®Ù\88Ù\85 Ø±Ø§Ø³Øª Ú©Ø±Ø¯Ù\88Ù\85اÙ\92 Ù\88 Ø§Ù\88 Ø¬Ø§Ù\86Û\8cاÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\92 Ø®Ù\88Ù\85 Ù\84اÙ\87اÙ\85سÛ\8aÙ\86اÙ\92 Ù\85Û\8cÙ\9bÙ\86 Ø³Ø§Ù\9bیل بٱرگ خوم",
+       "tog-watchdefault": "اÙ\9bزاÙ\81 Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\92Û\8cÙ±Ù\84 Ù\88 Ø¬Ø§Ù\86Û\8cاÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\92 Ù\85Ù\88 Ù\85ئÙ\86 Ø³Ø§Ù\9bÛ\8cÙ\84 Ø¨Ø±Ú¯Ù\88Ù\85 Ø¢Ù\84شدسÛ\8aÙ\86 Ú©Ø±Ø¯Ù\88Ù\85اÙ\92",
+       "tog-watchmoves": "اÙ\92زاÙ\81 Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\9bÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\92 Ø®Ù\88Ù\85 Ø¬Ø§ Ø¨ Ø¬Ø§Ø³Û\8aÙ\86 Ú©Ø±Ø¯Ù\88Ù\85اÙ\92 Ø³Û\8c Ø³Ø§Ù\9bÛ\8cل بٱرگوم",
+       "tog-watchdeletion": "اÙ\9bزاÙ\81 Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\92Û\8cÙ±Ù\84 Ù\88 Ø¬Ø§Ù\86Û\8cاÛ\8cÙ±Ù\84Û\8c Ú©Ø§Ù\92 Ø®Ù\88Ù\85 Ø² Ù\85Û\8cÙ\9bÙ\86 Ø³Ø§Ù\9bÛ\8cÙ\84 Ø¨Ù±Ø±Ú¯Ù\88Ù\85 Ù¾Ø§Ú©Ø³Ø§ Ú©Ø±Ø¯Ù\88Ù\85اÙ\92",
        "tog-watchuploads": "پٱرڤٱنداٛیٱل نۊئی کاْ باراْنم ڤاْ فاٛئرسڌ دیناگریٱل مو بالاڤٱن ڤۊ",
        "tog-watchrollback": "بالاڤٱن کرڌن بٱلٛگیٱلؽ کاْ اْؤورگٱرنیم ڤاْ فاٛئرسڌ دیناگریٱل مو",
-       "tog-minordefault": "دیاری کردن جۊر ڤیرایشتا ناقس",
-       "tog-previewontop": "دÛ\8cارÛ\8c Ú©Ø±Ø¯Ù\86 Ù¾Û\8cØ´ Ø³Ø¦Û\8cÙ\84 Ù¾Û\8cØ´ Ø² Ú¤Û\8cراÛ\8cشد جٱڤٱ",
-       "tog-previewonfirst": "دÛ\8cارÛ\8c Ú©Ø±Ø¯Ù\86 Ù¾Û\8cØ´ Ø³Ø¦Û\8cÙ\84 Ù\85ئÙ\86 Ù±Ú¤Ù\84Û\8cÙ\86 Ú¤Û\8cراÛ\8cشد",
-       "tog-enotifwatchlistpages": "Ù\87ر Ú¯Ø§ØªÛ\8c Ú©Ø§Ù\9b Û\8cÙ± Ø¨Ù±Ù\84Ú¯Ù± Û\8cا Û\8cÙ± Ø¬Ø§Ù\86Û\8cا Ù\85ئÙ\86 Ø³Ø¦یل بٱرگ مۊ آلشد ابۊ بوم خٱڤٱر بڌین",
+       "tog-minordefault": "دیاری کردن جۊر آلشدکاریٱل ناقس",
+       "tog-previewontop": "دÛ\8cارÛ\8c Ú©Ø±Ø¯Ù\86 Ù¾Û\8cØ´ Ø³Ø§Ù\9bÛ\8cÙ\84 Ù¾Û\8cØ´ Ø² Ø¢Ù\84شدکارÛ\8c جٱڤٱ",
+       "tog-previewonfirst": "دÛ\8cارÛ\8c Ú©Ø±Ø¯Ù\86 Ù¾Û\8cØ´ Ø³Ø§Ù\9bÛ\8cÙ\84 Ù\85Û\8cÙ\9bÙ\86 Ù±Ú¤Ù\84Û\8cÙ\86 Ø¢Ù\84شدکارÛ\8c",
+       "tog-enotifwatchlistpages": "Ù\87ر Ú¯Ø§ØªÛ\8c Ú©Ø§Ù\92 Û\8cÙ± Ø¨Ù±Ù\84Ú¯Ù± Û\8cا Û\8cÙ± Ø¬Ø§Ù\86Û\8cا Ù\85Û\8cÙ\9bÙ\86 Ø³Ø§Ù\9bیل بٱرگ مۊ آلشد ابۊ بوم خٱڤٱر بڌین",
        "tog-enotifusertalkpages": "گاتی کاْ بٱلگاْ کاریاریم آلشد ڤابی خٱڤٱروم کو",
        "tog-enotifminoredits": "او بٱلگاٛیٱل و جانیایٱلی کاْ ڤیرایشد کۊچیر و ناقس دارن بفرشن سی ٱنجوماناموم",
-       "tog-enotifrevealaddr": "دیاری کردن تیرنشۊن ٱنجوماناماْ مو مئن دیارکاری ایمیلی",
+       "tog-enotifrevealaddr": "دیاری کردن تیرنشۊن ٱنجوماناماْ مو میٛن دیارکاری ایمیلی",
        "tog-shownumberswatching": "دیاری کردن ٱندازاْ کاریارٱلی کاْ میٛن هال و بال سئیل کردن یا آلشدکارین",
-       "tog-oldsig": "اْمزا ایسئنی",
+       "tog-oldsig": "اْمزا ایسیٛنی",
        "tog-fancysig": "اٛمزایٱل ناتٱموم",
-       "tog-uselivepreview": "Ù¾Û\8cØ´ Ø³Ø¦یل زندٱ نٱ ڤٱن ڤا کار",
+       "tog-uselivepreview": "Ù¾Û\8cØ´ Ø³Ø§Ù\9bیل زندٱ نٱ ڤٱن ڤا کار",
        "tog-forceeditsummary": "موناْ میٛن گاتی کاْ ڤیرایشت ناقس یا هالی ٱنجوم اْبو خٱڤٱر کو",
-       "tog-watchlisthideown": "Ù\82اÙ\85 Ú©Ø±Ø¯Ù\86 Ø¢Ù\84شدکارÛ\8cاÙ\85 Ø² Ø³Ø¦یل بٱرگ",
-       "tog-watchlisthidebots": "Ù\82اÙ\85 Ú©Ø±Ø¯Ù\86 Ø¢Ù\84شدکارÛ\8cÙ±Ù\84 Ø¨Ù\88ت Ø² Ø³Ø¦یل بٱرگ",
-       "tog-watchlisthideminor": "Ù\82اÙ\85 Ú©Ø±Ø¯Ù\86 Ø¢Ù\84شدکارÛ\8cÙ±Ù\84 Ú©Û\8aÚ\86Û\8cر Ø² Ø³Ø¦یل بٱرگ",
+       "tog-watchlisthideown": "Ù\82اÙ\85 Ú©Ø±Ø¯Ù\86 Ø¢Ù\84شدکارÛ\8cاÙ\85 Ø² Ø³Ø§Ù\9bیل بٱرگ",
+       "tog-watchlisthidebots": "Ù\82اÙ\85 Ú©Ø±Ø¯Ù\86 Ø¢Ù\84شدکارÛ\8cÙ±Ù\84 Ø¨Ù\88ت Ø² Ø³Ø§Ù\9bیل بٱرگ",
+       "tog-watchlisthideminor": "Ù\82اÙ\85 Ú©Ø±Ø¯Ù\86 Ø¢Ù\84شدکارÛ\8cÙ±Ù\84 Ú©Û\8aÚ\86Û\8cر Ø² Ø³Ø§Ù\9bیل بٱرگ",
        "tog-watchlisthideliu": "قام کردن آلشدکاریٱل ٱنجوم گرهڌاْ ڤا دٱسد کاریاریٱلی کاْ ٱڤوڌناْ ڤامین ز سئیل بٱرگ",
        "tog-watchlistreloadautomatically": "راتؽ کاْ یٱ پاْلایاٛ آلشڌآڤیڌ فاٛئرسڌ دیناگری بؽنگوڌ(خوڌکار)ڤ رۊز ڤۊهاْ(هوجاْ ڤاْ جاڤا اسکریپت)",
        "tog-watchlistunwatchlinks": "فرٱنیڌن دیاری کونٱنڌیٱل نڤیڌ دیناگری/دیناگری ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}})ڤاْ بٱلٛگیٱل دیناگری آلشڌدار (سی عملیات کلؽز ۉ بلؽز کرڌن، جاڤاسکریپت   هوجاْ هؽڌآ)",
        "tog-watchlisthideanons": "قام کردن آلشدکاریٱل ٱنجوم گرهڌاْ ڤا دٱسد کاریاریٱلی کاْ نٱڤوڌناْ ڤامین ز سئیل بٱرگ",
-       "tog-watchlisthidepatrolled": "تٱپنیڌن پٱخڌارٱل ساوا ز لیسڌ دیناگریٱل",
+       "tog-watchlisthidepatrolled": "تٱپنیڌن پٱخڌارٱل ساوا ز ساٛیل بٱرگ دیناگریٱل",
        "tog-watchlisthidecategorization": "تٱپنیڌن رٱئڌڤٱنی بٱلٛگیٱل",
        "tog-ccmeonemails": "باْسی کردن کوپیٱل ٱنجوماناماْیٱلی کاْ مو فرشنوم سی باقی کاریارٱل",
-       "tog-diffonly": "بٱÙ\84گاÙ\9bÛ\8cÛ\8c Ù\86Ù± Ú©Ø§Ù\9b Ù\85ئÙ\86Û\8aÙ\86اÙ\9b Ù\81ٱرخداراÙ\9b نشۊن مٱڌاٛ",
-       "tog-showhiddencats": "دÛ\8cارÛ\8c Ú©Ø±Ø¯Ù\86 Ø¬Ù±Ø±ØºØ§Ù\9b بٱندیٱل نادیار",
+       "tog-diffonly": "بٱÙ\84گاÙ\92Û\8cÛ\8c Ù\86Ù± Ú©Ø§Ù\92 Ù\85Û\8cÙ\9bÙ\86Û\8aÙ\86اÙ\92 Ù\81ٱرخداراÙ\92 نشۊن مٱڌاٛ",
+       "tog-showhiddencats": "دÛ\8cارÛ\8c Ú©Ø±Ø¯Ù\86 Ø¬Ù±Ø±ØºØ§Ù\92 بٱندیٱل نادیار",
        "tog-norollbackdiff": "دینا زاْ ڤاگٱرڌونی نٱچی یٱکی نماونیڌاْ ڤۊهاْ",
        "tog-useeditwarning": "رات ؤردٱر کرڌن زاْ بٱلٛگاْ آلشڌکاری ڤاْ ری داشڌن آلشڌکاریٱل کۊنڤیڌاْ ڤ مو مناراٛ(هوشڌار)داڌاْ ڤۊهاْ",
        "tog-prefershttps": "راتؽ کاْ ڤامؽن ساماناْ آڤیڌم هاٛ زاْ مؽنڌاری ٱمن اْسفاْڌاْ ڤۊهاْ",
        "underline-always": "همیشٱ",
        "underline-never": "هیژگات",
-       "underline-default": "Ù¾Û\8aسداÙ\9b Û\8cا Ø¯Ú¤Ù\88ارتاÙ\9b Ù\86Û\8cٱر Ù\85Û\8cزÛ\8aÙ\86کارÛ\8c Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\9b",
+       "underline-default": "Ù¾Û\8aسداÙ\92 Û\8cا Ø¯Ú¤Ù\88ارتاÙ\92 Ù\86Û\8cٱر Ù\85Û\8cزÛ\8aÙ\86کارÛ\8c Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92",
        "editfont-style": "بارت قلٱم جٱڤاْ آلشڌکاری:",
        "editfont-monospace": "قاٛلٱم ڤا تلاهاْ بؽڌٱگ(سابت)",
-       "editfont-sansserif": "Ù\81Ù\88Ù\86ت Ø³Ø§Ù\86 Ø³Ø¦Ø±Û\8cÙ\81",
+       "editfont-sansserif": "فونت سان سریف",
        "editfont-serif": "فونت سريف",
        "sunday": "یٱشٱمبڌ",
        "monday": "دۊشٱمبڌ",
        "category-article-count": "{{PLURAL:$2|ای دٱسداْ فٱقٱت ز ڤٱرگرهڌاْ بٱلگاْ نیاییاْ.|ای دٱسداْ ز ڤٱرگرهڌاْ {{PLURAL:$1|بٱلگاْ|$1 بٱلگاْیٱل}} میٛن ای دٱسدٱ, بیشتر ز کول $2 .}}",
        "category-article-count-limited": "پیشتٱر گهڌاْ ڤابیڌاْ {{PLURAL:$1|بٱلگاْ|$1 بٱلگاْیٱل میٛنس هڌن}} مئن او دٱسداْ جاری.",
        "category-file-count": "{{PLURAL:$2|ای دٱسداْ فٱقٱت ز ڤٱرگرهڌاْ جانیایٱل نیاییاْ.|ای دٱسداْ ز ڤٱرگرهڌاْ {{PLURAL:$1|جانیا|$1 جانیایٱل}} ای دٱسداْ, بیشتر ز کول $2 .}}",
-       "category-file-count-limited": "پیشتٱر گهڌاْ ڤابیڌاْ {{PLURAL:$1|جانیا|$1 جانیایٱل میٛنس هڌن}} مئن او دٱسداْ جاری.",
+       "category-file-count-limited": "پیشتٱر گهڌاْ ڤابیڌاْ {{PLURAL:$1|جانیا|$1 جانیایٱل میٛنس هڌن}} میٛن او دٱسداْ جاری.",
        "listingcontinuesabbrev": "دینداگٱرد",
        "index-category": "بٱلٛگیٱل نماو نڤؽڌاْ",
        "noindex-category": "بٱلٛگاْیٱل نمایاْ ناڤیڌاْ",
        "returnto": "ڤورگٱشدن ب $1.",
        "tagline": "ز {{SITENAME}}",
        "help": "رٱنمۊنی",
+       "help-mediawiki": "هومیاری سی مدیاڤیکی",
        "search": "پاٛی جۊری",
        "search-ignored-headings": "#<!-- ای بٱلٛگاْ ناْ هٱمی بٱرتی کاْ هؽڌا رها کونین --> <pre>\n# سٱرتال ٱلؽ  که ڤا موڌی نیڌاْ اْڤۊهن\n# تٱسیر آلشڌ راتؽ ایاهاْ ڤارۊ کاْ بٱلٛگاْ هاڤی او سٱرتال، نماواْ ڤۊهاْ.\n# اؽسا تٱرین ڤا ٱنجوم یٱ آلشڌکاری پیک بٱلٛگاْ ناْ بؽرٱ ڤاْ دو کرات نماواْ ڤیڌن کونین\n# رڤشڌس چونوݩ هؽڌآ:\n#  *هٱر چاْ زاْ یٱ هؽلنیڌاْ «#» تا تٱهاْ هؽل ڤاهین، یٱ اشکافنیڌن هؽڌا.\n#  *هٱر هؽل بؽ تلاهاْ، دٱیخٱن اوڌڤانی  هؽڌا کاْ نیڌاْ گریڌاْ اْڤۊهاْ (ڤا رعایٱت گاپی ۉ ساوائی هٱرفٱل).\nکونڳگٱیٱل(مٱنابع)\nمؽنڌاری ڤا دٱر\nهٱم چونوݩ ڤنٱرین\n#</pre> <!-- leave this line exactly as it is -->",
        "searchbutton": "پاٛی جۊری",
        "edit-local": "آلشڌکاری اشکافنیڌیٱل بۊمی",
        "create": "راس كردن",
        "create-local": "یٱ تۉزی ڤولات نشینی اْزاف کونین",
-       "delete": "حذف",
+       "delete": "پاکسا کردن",
        "undelete_short": "جوݩنیڌن(اْئیا){{PLURAL:$1|یٱ آلشڌکاری|$1 آلشڌکاری}}",
        "viewdeleted_short": "{{PLURAL:$1|پژارنیڌن یٱ پٱخڌار|$1 پژارنیڌن پخڌارٱل}}",
-       "protect": "حفاظت وحمایت",
-       "protect_change": "Ø¢Ù\84إشت Ú©Ù\88Ù\86Ù\8aÙ\86",
+       "protect": "هناڌاری ۉ پٱلڌاری",
+       "protect_change": "آلشت کونين",
        "unprotect": "هناڌاری زاْ بٱلٛگاْ",
        "newpage": "بٱلگاْ تازاْ",
        "talkpagelinktext": "چٱک چنٱ",
-       "specialpage": "صÙ\81Ø­Ù\87 Ù\85خصÙ\88ص",
+       "specialpage": "بٱÙ\84گاÙ\92 Ú¤Û\8cجٱ",
        "personaltools": "ٱڤزارٱل شٱسقی",
        "talk": "گٱپ",
        "views": "دیڌنی یٱل",
-       "toolbox": "ٱۋزارٱل",
+       "toolbox": "ٱڤزارٱل",
        "tool-link-userrights": "آلشڌ بنکۊیٱل {{GENDER:$1|مؽنتور}}",
        "tool-link-userrights-readonly": "نماونیڌن بنکۊیٱلاٛ {{GENDER:$1|مؽنتور}}",
        "tool-link-emailuser": "کل کرڌن ناماٛ ڤ {{GENDER:$1|مؽنتور}}",
        "imagepage": "نیٱشڌن بٱلٛگاْ هیاراٛ\nنیٱشڌن بٱلٛگاْ فایل",
-       "mediawikipage": "دیدن صفحه پیام",
-       "templatepage": "دÛ\8cدÙ\86 ØµÙ\81Ø­Ù\87 Ù\82اÙ\84ب",
-       "viewhelppage": "دیدن صفحه کمک",
-       "categorypage": "دÛ\8cدÙ\86 ØµÙ\81Ø­Ù\87 Ø·Ø¨Ù\82Ù\87 Ø¨ندی",
-       "viewtalkpage": "دیدن بحث ها",
+       "mediawikipage": "دیڌن بٱلگاْ پاٛیغوم",
+       "templatepage": "دÛ\8cئÙ\86 Ø¨Ù±Ù\84Ú¯Ù± Ú\86Û\8aاÙ\92",
+       "viewhelppage": "دیڌن بٱلٛگاْ هومیاری",
+       "categorypage": "دÛ\8cئÙ\86 Ø¨Ù±Ù\84گاÙ\92 Ø¯Ù±Ø³Ø¯Ø§Ù\92 Ø¨Ù±ندی",
+       "viewtalkpage": "دیڌن چٱک چنٱ",
        "otherlanguages": "میٛن زڤونا دیٱ",
        "redirectedfrom": "(ڤاگٱردۊنی سی $1)",
-       "redirectpagesub": "صÙ\81Ø­Ù\87 ØªØµØ­Û\8cØ­ Ù\88Ù\87داÛ\8cت Ù\85جدد",
+       "redirectpagesub": "بٱÙ\84گاÙ\92 Ú¤Ø§Ú¯Ù±Ø±Ø¯Û\8aÙ\86Û\8c",
        "redirectto": "ڤاگٱردۊنی سی:",
        "lastmodifiedat": "ای بٱلگٱ تازاٛییا ماٛن $1 و میٛن $2 آلشدکاری ڤابیڌاْ.",
-       "viewcount": "این صفحه قابل دسترسی شده است {{PLURAL:$1|once|$1 times}}.",
+       "viewcount": "ای بٱلگاْ میٛن دٱسرساْ {{PLURAL:$1|یٱ کاْرٱت|$1 چٱن کاْرٱت}}.",
        "protectedpage": "بٱلگاْ پٱر و پیم ڤابیڌاْ",
        "jumpto": "پراٛستن سی:",
        "jumptonavigation": "ناڤجۊری",
        "jumptosearch": "جوستن",
-       "view-pool-error": "مونٱسفاناٛ سرڤرٱل ایساْ دیلٛ ڤارٱزافاْ هؽڌن\nبورفرٱی زاْ مؽنتورٱل هنؽ تٱقلا اْکونن کاْ ای بٱلٛگاْ ناْ ساٛل کونن.\nلوتفٱن نهاتٱر زاْ تٱقلا دوکرت سی ساٛل کرڌن ای بٱلٛگاْ قٱلیلؽ دٱسنیٱ کونین",
+       "view-pool-error": "ز بٱخت گٱن سرڤرٱل ایساْ دیلٛ ڤارٱزافاْ هؽڌن\nبورفرٱی زاْ مؽنتورٱل هنؽ تٱقلا اْکونن کاْ ای بٱلٛگاْ ناْ ساٛل کونن.\nلوتفٱن نهاتٱر زاْ تٱقلا دوکرت سی ساٛل کرڌن ای بٱلٛگاْ قٱلیلؽ دٱسنیٱ کونین",
        "generic-pool-error": "موتٱسفاْناٛ سرڤرٱل ایساْ دوچار ڤارٱزافی آڤیڌناْ.\nبورفرٱئی زاْ مؽنتوروݩ هنؽ تٱقلا اْکونن کاْ  ای بٱلٛگاْ ساٛل کونن.\nلوتفٱن نهاتر زاْ تقلادوکرت سی ساٛل کرڌن ای بٱلٛگاْ قٱلیلؽ دٱس نیٱڌارین",
        "pool-timeout": "تٱ هٱرت تؽڤرٱئی سی قولف",
        "pool-queuefull": "یٱتور کۊگٱ مٱشلٱق هؽڌا",
-       "pool-errorunknown": "خطا نادیار",
+       "pool-errorunknown": "خٱتا نادیار",
        "pool-servererror": "سرڤیس باٛنٱتگر کۊگٱ ڤ دٱسرٱس نؽڌا",
        "poolcounter-usage-error": "خٱتای اْسفاْڌاْ:$1",
        "aboutsite": "راجوڤ ب {{SITENAME}}",
        "disclaimers": "تی پۊشنیڌنیٱل",
        "disclaimerpage": "Project: تی پۊشنیڌنی کولی",
        "edithelp": "هومیاری سی آلشدکاری",
-       "helppage-top-gethelp": "هومیاري",
+       "helppage-top-gethelp": "هومیاری",
        "mainpage": "سرتال",
        "mainpage-description": "سرتال",
-       "policy-url": "Project:خط Ù\85Ø´Û\8c",
-       "portal": "سرآسۊناْ کاریارۊن",
-       "portal-url": "Project:سرآسۊناْ کاریارۊن",
+       "policy-url": "Project:رٱڤشت Ú©Ø§Ø±",
+       "portal": "سرآسۊناْ کاریاروݩ",
+       "portal-url": "Project:سرآسۊناْ کاریاروݩ",
        "privacy": "رٱدیارکونی رازڤادٙاری",
        "privacypage": "Project:رٱدیارکونی رازڤاڌاری",
-       "badaccess": "خطا :اجازه بگیر",
-       "badaccess-group0": "اÛ\8cسا Ø§Ø¬Ø§Ø²Ù\87 Ø§Ù\86جاÙ\85 Ú©Ø§Ø±Û\8c Ú©Ù\87 Ø®Ù\88استÛ\8cÙ\86 Ø±Ø§ Ù\86دارین",
-       "badaccess-groups": "او کاری که ایسا درخواست کردین فقط سی کاربرانیه که من ای  گروهن\n{{PLURAL:$2|آن گروه|یکی زه گروهها}}: $1.",
-       "versionrequired": "Û\8cÙ\87 Ù\86سخÙ\87 Ø²Ù\87 Ù\86Û\8cازÙ\85Ù\86دÛ\8cÙ\87اÛ\8c Ù\88Û\8cÚ©Û\8c Ù\85دÛ\8cا\n$1",
-       "versionrequiredtext": "Û\8cÙ\87 Ù\86سخÙ\87 Ø²Ù\87 Ù\88Û\8cÚ©Û\8c Ù\85دÛ\8cا($1) Ù\86Û\8cازÙ\85Ù\86د Ù\87 Ø¨Ù\87 Ø§Ø³ØªÙ\81ادÙ\87 Ø²Ù\87 Ø§Û\8c ØµÙ\81Ø­Ù\87\nبÙ\88Û\8cÙ\86 :[[Special:Version|version page]].",
-       "ok": "خووه",
+       "badaccess": "خٱتا :ساْلا بیار",
+       "badaccess-group0": "اÛ\8cسا Ø³Ø§Ù\92Ù\84ا Ù±Ù\86جÙ\88Ù\85 Ø¯Ø§Ú\8cÙ\86 Ú\86Ù\88Ù\86Ù\88 Ú\86Û\8c Ú©Ø§Ù\92 Ø®Ø§Û\8cÙ\86 Ù\86ارین",
+       "badaccess-groups": "او کاری کاْ ایسا خاسدیناْ فٱقٱت کاریارٱلی کاْ میٛن جٱرغاْن تٱرن کردین \n{{PLURAL:$2|او جٱرغاْ|یکی ز جٱرغاْیٱل}}: $1.",
+       "versionrequired": "Û\8cÙ± Ù\86Ù\88سÙ\82اÙ\92 $1 Ø² Ú\86Û\8cا Ù\84ازÙ\88Ù\85 Ú¤Û\8cÚ©Û\8c Ù\85دÛ\8cا",
+       "versionrequiredtext": "Û\8cÙ± Ù\86Ù\88سÙ\82اÙ\92 Ø² Ú\86Û\8cا Ù\84ازÙ\85 Ú¤Û\8cÚ©Û\8c Ù\85دÛ\8cا($1) Ú©Ø§Ù\92 Ù\87اÛ\8cÙ\86 Ù\85Û\8cÙ\9bÙ\86 Ø§Û\8c Ø¨Ù±Ù\84گاÙ\92 Ú¤Ù±Ù\86Û\8cÙ\86سÙ\88Ý© Ú¤Ø§ Ú©Ø§Ø±\n:[[Special:Version|version page]].",
+       "ok": "خۊڤاْ",
        "pagetitle-view-mainpage": "سرصفحه",
        "retrievedfrom": "دوڤارتاْ جۊری ز \"$1\"",
-       "youhavenewmessages": "پیام تاره داری $1 ($2).",
+       "youhavenewmessages": "{{PLURAL:$3|ایسا دارین}} $1 ($2).",
        "youhavenewmessagesfromusers": "اؽسا زاْ{{PLURAL:$3|یٱ مؽنتور داٛهر|$3  مؽنتور}} $1 دارین ($2).",
        "youhavenewmessagesmanyusers": "اؽسا زاْ بورؽ مؽنتور $1 دارین($2).",
        "newmessageslinkplural": "\n{{PLURAL:$1|پاٛخوم نۊ|999=پاٛخومٱل نۊ}}",
-       "newmessagesdifflinkplural": "$1 {{PLURAL:$1|آلشڌ|آلشڌا}}",
-       "youhavenewmessagesmulti": "اÛ\8cسا Ù¾Û\8cاÙ\85 ØªØ§Ø²Ù\87 Ø¯Ø§Ø±Û\8cÙ\86 Ù\85Ù\86Ù\87\n$1",
+       "newmessagesdifflinkplural": "$1 {{PLURAL:$1|آلشڌ|آلشڌٱل}}",
+       "youhavenewmessagesmulti": "اÛ\8cسا Ù\85Û\8cÙ\9bÙ\86 $1 Û\8cÙ± Ù¾Ø§Ù\9bÛ\8cغÙ\88Ù\85 ØªØ§Ø²Ø§Ù\92 Ø¯Ø§Ø±Û\8cÙ\86",
        "editsection": "آلشدکاری کردن",
        "editold": "آلشدکاری کردن",
-       "viewsourceold": "دیدن منبع",
+       "viewsourceold": "دیڌن سرچشمٱ",
        "editlink": "آلشدکاری کردن",
-       "viewsourcelink": "سئیل سرچشماْ کونین",
+       "viewsourcelink": "ساÙ\9bیل سرچشماْ کونین",
        "editsectionhint": "آلشدکاری بٱرجا: $1",
        "toc": "مینۊناْیٱل",
-       "showtoc": "نمایش",
-       "hidetoc": "قایم",
-       "collapsible-collapse": "جأم كردن",
-       "collapsible-expand": "گأپ کلۈن کردن",
+       "showtoc": "دیاری کردن",
+       "hidetoc": "قام کردن",
+       "collapsible-collapse": "جٱم كردن",
+       "collapsible-expand": "گٱپ کلۊن کردن",
        "confirmable-confirm": "آسی موتمٱعن {{GENDER:$1|هؽڌین}}؟",
-       "confirmable-yes": "هرإ",
-       "confirmable-no": "نأ",
-       "thisisdeleted": "دیدن یا اعاده $1?",
-       "viewdeleted": "دیدن$1?",
-       "restorelink": "{{PLURAL:$1|پاک کردن یه اصلاح|$1 پاک کردن اصلاحات}}",
-       "feedlinks": "تغذیه:",
-       "feed-invalid": "اشتراک  Ø²Ù\87 Ø±Ø§Ù\87  ØªØ§Û\8cÙ¾ Ø¨Ø§Ø·Ù\84Ù\87",
+       "confirmable-yes": "هٱراْ",
+       "confirmable-no": "نٱ",
+       "thisisdeleted": "دیڌن یا ڤورگٱنیڌن $1؟",
+       "viewdeleted": "دیڌن$1؟",
+       "restorelink": "{{PLURAL:$1|پاکسا کرن یٱ آلشدکاری|$1 پاکسا کردن آلشدکاریٱل}}",
+       "feedlinks": "هٱڤال خۊ:",
+       "feed-invalid": "جÛ\8aر Ù\87Ù\88Ù\85بٱر Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92 Ù\87ٱڤا Ø®Û\8a Ù\86ادÛ\8cار",
        "feed-unavailable": "خوراکٱل هوڤال خوݩ ڤاْ دٱسرٱس نؽڌا",
-       "site-rss-feed": "خبرخو RSS سی $1",
-       "site-atom-feed": "هٱڤال خۊنی Atom سی $1",
-       "page-rss-feed": "خبرخو RSS سی «$1»",
-       "page-atom-feed": "هٱڤال خۊن Atom سی $1",
+       "site-rss-feed": "هٱڤال خۊ RSS سی $1",
+       "site-atom-feed": "هٱڤال خونی Atom سی $1",
+       "page-rss-feed": "هٱڤال خۊ RSS سی «$1»",
+       "page-atom-feed": "هٱڤال خۊ Atom سی $1",
        "red-link-title": "$1 (چونو بلگاْیی نیڌس)",
        "sort-descending": "ڤدین یٱک کٱم آڤیڌن",
+       "sort-ascending": "پوشت سریٱک کم ڤابیڌن",
        "nstab-main": "بٱلگٱ",
        "nstab-user": "بٱلگاْ کاریار",
-       "nstab-media": "بÙ\84گأ Ú¤Ø§Ø±Ø³Ú¯Ø£Ø±Ù\8a",
+       "nstab-media": "بٱÙ\84گاÙ\92 Ú¤Ø§Ø±Ø³Ú¯Ù±Ø±Û\8c",
        "nstab-special": "بٱلگاْ ڤیجٱ",
        "nstab-project": "بٱلگاْ پوروجاْ",
        "nstab-image": "جانیا",
        "nstab-mediawiki": "پیغوم",
        "nstab-template": "چۊاْ",
-       "nstab-help": "بألگإ هومیاری",
+       "nstab-help": "بٱلگاْ هومیاری",
        "nstab-category": "دٱسدٱ",
        "mainpage-nstab": "سرتال",
        "nosuchaction": "چونوݩ عملؽ نؽڌآ",
        "nosuchactiontext": "عمل دزاْ آڤیڌاْ ڤاْ تیرنشون مؽنتوری بؽ اْتڤار هؽڌآ\nمومکن هؽڌآ تیرنشون مؽنتوری ناْ پٱلارنیڌ ڤامؽ کرڌاْ ڤۊهین یا دینا یٱ پاٛڤٱن بٱی ڌارؽ ناْ گریڌیناْ.\nهٱمچونوݩ مومکن هؽڌآ بٱی ڤ نٱرم ٱفزار ڤ کارگریداْ آڤیڌاْ در{{SITENAME}} ڤیڌاْ ڤۊهاْ",
-       "nosuchspecialpage": "Ú\86Ù\88Ù\86Ù\88 Ø¨Ù\84گاÙ\9b Û\8bÛ\8cجاÙ\9bیی دیاری نیکوناٛ",
+       "nosuchspecialpage": "Ú\86Ù\88Ù\86Ù\88 Ø¨Ù\84گاÙ\92Û\8cÛ\8c Ú¤Û\8cجاÙ\92یی دیاری نیکوناٛ",
        "nospecialpagetext": "<strong>اؽسا یٱ بٱلٛگاْ ڤیژاٛ بؽ اٛتڤار ناْ خاسیناْ.</strong>\n\nفاٛئرسڌاْ زاْ بٱلٛگیٱل ڤیژاٛ موجاز ڤ[[Special:SpecialPages|{{int:specialpages}}]] هؽڌا.",
-       "error": "خطا",
+       "error": "خٱتا",
        "databaseerror": "خٱتای ستینگٱ داڌاْ",
        "databaseerror-text": "بٱیؽ مؽن پورسایشڌ زاْ ستینگٱ داڌاْ آوؤڌ ڤارو.هؽ اْمکوݩ ڌاراْ دیاریکوناْ بٱیؽ مؽن نٱرم ٱفزار ڤۊهاْ",
        "databaseerror-textcl": "یٱ خٱتا مؽن پورسایشڌاْ ستینگٱ داڌا آوؤڌ ڤارو",
        "databaseerror-query": "جوستکاری: $1",
        "databaseerror-function": "دینارۉ$1",
-       "databaseerror-error": "خطا: $1",
+       "databaseerror-error": "خٱتا: $1",
        "transaction-duration-limit-exceeded": "سی پٱرؽز زاْ ؤرکل آڤیڌن تم رو مؽن نوسخؤرڌاری، ای تٱراکونش زاْ کارؤٱس چیناکاْ رات هؽلنیڌن($1) زاْ هنا$2 {{PLURAL:$2|سانیاْ|سانیاْ}} ڤؽشڌٱر ڤیڌ. ٱر هنؽسین چیٱل فرٱیناْ هومؤٱخڌ آلشڌ اْکونین،پلرڌ کونین ڤ جا ای کار چٱن اْخڌام ناْ مؽن جٱرخیٱل ساواتٱر ٱنجوم ڤڌین",
        "laggedslavemode": "\"ؤیرڌار\" بٱلٛگاْ مومکن هؽڌا ڤ نهارٱسونیٱل ٱخیرناْ ڤا ؤٱرنگراْ",
        "readonly": "ستینگٱ داڌاْ بؽرٱ آڤیڌ",
        "readonlytext": "ستینگٱ داڌاْ اؽساْ ؤر ؤٱر ؤرکل آڤیڌن مٱڤٱرڌ نۊ ۉ بٱخیاْ آلشڌٱل بؽرٱ آڤیڌاْ:ای ؤٱزیٱت اْئتمالٱن ڤ سی باٛئخڌٱرسازی ۉ سٱرآوریٱل مٱعمول هؽڌا کاْ دیناتٱر زاْ او ؤٱز ڤ بارت نهائی اْؤرگٱرڌن.\nدیڤونڌارؽ کاْ هو ناْ بؽرٱ کرڌاْ ای اْشکافنیڌن تیژنیڌاْ$1",
        "missingarticle-rev": "(ڤانیٱری#: $1)",
        "missingarticle-diff": "(فٱرخ: $1, $2)",
-       "nonwrite-api-promise-error": "سٱرآینڌ اْچ‌تی‌تی‌پی 'Promise-Non-Write-API-Action' کلٛ آڤیڌ ؤلی دٱرخاس ڤاْ یٱ مؽنڌار کۊدال نڤیسی پۊڌمان نڤشڌن ڤی.",
-       "internalerror": "خطا مإنجقایي",
+       "readonly_lag": "ای رسیناْگٱ قولف ڤابیڌاْ تا گاتی کاْ سرڤر رسیناْگٱ برساْ ب جاگاْ ٱسلی خوس.",
+       "nonwrite-api-promise-error": "سٱرآینڌ اْچ‌تی‌تی‌پی 'Promise-Non-Write-API-Action' کلٛ آڤیڌ ؤلی دٱرخاس ڤاْ یٱ مؽنڌار کۊدال نڤیسی ماجۊل نڤشڌن ڤی.",
+       "internalerror": "خٱتا میٛنجقای",
        "internalerror_info": "خاٛتای مؽنی",
        "internalerror-fatal-exception": "اْستسنای چۊلگٱر زاْ نۉع «$1»",
        "filecopyerror": "نڤیڌ زاْ بٱلٛگکۊ«$1» ری «$2» لفگری آڤۊهاْ.",
        "directoryreadonlyerror": "لاٛک «$1» تاٛنا خؤٱننی هؽڌآ",
        "directorynotreadableerror": "لاٛک «$1» قاڤل خؤٱناْ نؽڌآ",
        "filenotfound": "ناڤیڌ بٱلٛگکۊ «$1» پژار ڤۊهاْ",
+       "unexpected": "ٱزایشت ناخاستاْ: \"$1\"=\"$2\".",
        "formerror": "خاٛتا:نٱتری فورم ناْ کل کونی",
        "badarticleerror": "نٱتری ای کار ناْ ؤر ای بٱلٛگاْ ٱنجوم ڤڌی",
        "cannotdelete": "امکان پژار بٱلٛگاْ یا شؽڤات$1 نؽڌآ\nمومکن هؽڌآ نهاتٱر کسؽ داٛهراْ هونه پژارنیڌاْ",
        "cannotdelete-title": "نٱتری بٱلٛگاْ$1 ناْ پژار کرڌ",
        "delete-scheduled": "بٱلٛگاْ$1 سی پاکسا کۊڌالکاری آڤیڌاْ.لوتفٱن سٱڤۊر ڤۊهین",
        "delete-hook-aborted": "پژار ڤا قولاڤ لٱق آڤیڌ\nاشکافنیڌنی سی هؽ داڌ نڤیڌ",
-       "badtitle": "عنوان بد",
+       "no-null-revision": "سی بٱلگاْ $1 ڤانیٱری خومسا ناْ راس کونین",
+       "badtitle": "داسۊن گٱن",
        "badtitletext": "داسۊن خاسدنی نادیار، هالی، یا داسۊنی کاْ میٛنجقا زڤونی یا میٛنجقا ڤیکی ڤا هومپاٛیڤٱند دوروسد ناراْ و یا گاشا چٱنتا کاراکتر داراْ کاْ ڤا میٛن داسۊن نۉ باْیوفتاْ ڤا کار.",
        "title-invalid-empty": "اوڌڤان بٱلٛگاْ دٱرخاس آڤیڌاْ پٱتی هؽڌآ یا تاٛنا اوڌڤان مؽن نوم گوڌ آڤیڌاْ هؽڌآ",
        "title-invalid-utf8": "اوڌڤان بٱلٛگاْ دٱرخاس آڤیڌاْ هؽل ڤیڌاْ نادوروس یونیکوڌ هؽڌآ",
        "title-invalid-talk-namespace": "اوڌڤان بٱلٛگاْ خاساْ ڤیڌا ڤاْ بٱلٛگاْ بٱئسؽ کاْ نؽڌآ اْرجا اْکوناْ",
        "title-invalid-characters": "اوڌڤان بٱلٛگاْ خاساْ آڤیڌاْ هؽلڤیڌاْ ناموجاز داراْ$1",
        "title-invalid-relative": "اوڌڤان دارای دزاٛئی هؽڌآ.اوڌڤانٱل کاْ دزاٛئی نسمی(چی /.یا/...) ناموجاز هؽڌن چیناکاْ ڤا موڌیگر مؽنتورٱل قاڤل دٱسرٱسی نؽڌن",
-       "viewsource": "دیئن سرچشمٱ",
+       "title-invalid-magic-tilde": "داسۊن بٱلگاْ خاسدنی میٛنۊناْ دار یٱ نیٛماجا جاڌۊیی نادیاْ.(<nowiki>~~~</nowiki>).",
+       "title-invalid-too-long": "اوڌڤان بٱلٛگاْ دٱرخاس آڤیڌاْ فرٱ دورهؽڌآ.. نڤا زاْ $1 {{PLURAL:$1|بایت|بایت}} مؽن رازیناْنهاڌن یوتی‌اف-۸ ڤؽشڌٱر ڤۊهاْ.",
+       "title-invalid-leading-colon": "داسۊن بٱلگاْ خاسدنی میٛنۊناْ دار یٱ کولون نادیار ز ٱڤٱل کارساْ.",
+       "perfcached": "راْسیناْیٱل نیایی ز ڤیرگٱ قام ڤابیڌاْ مۊکیشت ڤابیڌناْ و گاشا ب هاْنگوم سازی نٱڤابیناْ. بیشترۊناْ {{PLURAL:$4|یٱ نتیجاْ|$4 کول نتیجاْیٱل}} کاْ ز ڤیرگٱ قام ڤابیناْ میٛن دٱسرس بۊن.",
+       "perfcachedts": "راْسیناْیٱل نیایی ز ڤیرگٱ قام ڤابیڌاْ مۊکیشت ڤابیڌناْ و گاشا ب هاْنگوم سازی نٱڤابیناْ. بیشترۊناْ {{PLURAL:$4|یٱ نتیجاْ|$4 کول نتیجاْیٱل}} کاْ ز ڤیرگٱ قام ڤابیناْ میٛن دٱسرس بۊن.",
+       "querypage-no-updates": "نٱترین ای بٱلگاْ ناْ ب هاْنگوم سازی کونین.\nرسیناْیٱل ایچو تازاْکاری نٱڤابیڌناْ.",
+       "viewsource": "دیڌن سرچشمٱ",
+       "viewsource-title": "ساٛیل سرچشماْ $1 کونین",
        "actionthrottled": "نها کار اؽسا گریداٛ آڤیڌ",
        "actionthrottledtext": "ڤ سی نهاگری زاْ ؤولٱ ڤیڌن چۊلکاری،اْجازاٛ نارین کاْ چونوݩ کارؽ ناْ ڤؽشڌر زاْ چٱن کرٱت ڤ یٱ رات کول ٱنجوم ڤڌین\nلوتفٱن دیناتٱر زاْ چٱن دٱیخاْ سٱرزنۊ پلرڌ کونین",
        "protectedpagetext": "ای بٱلٛگاْ سی نهاگری راْ آلشڌکاری یا جومجیل دهرؽ پلڌاری آڤیڌاْ",
-       "viewsourcetext": "ایسا ترین سرچشماٛ ای بلگاٛ نٱ هم بنیٱرین و هم ۋوردارینس:",
-       "invalidtitle": "داسۊن بی ٱرزشد",
-       "exception-nologin": "هٱنی نٱڤۊڌیناٛ ڤامئن",
+       "viewsourcetext": "ایسا تاْرین سرچشماْ ای بلگاْ ناْ هٱم بنیٱرین و هم ڤوردارینس:",
+       "viewyourtext": "ایسا تاْرین یٱ کوپی ز سرچشمٱ<strong>آلشدکاریٱل خوتوݩ</strong> ڤوردارین سی ای بٱلگاْ",
+       "protectedinterface": "ای بٱلگاْ سی نٱرم ٱفزاری کاْ سی ڤیکی نڤیسی هڌ آماڌاْ ڤابیڌاْ،و ز موزاهمٱت کاری پٱر و پیم ڤابیڌاْ سی اْزاف کردن یا آلشدکاری کردن میٛن هٱماْ ڤیکیٱل لوتف کونین [https://translatewiki.net/ translatewiki.net] ناْ ڤٱنین ڤا کار، پوروجاْ ڤولات نشین سازی ڤیکیمدیا.",
+       "editinginterface": "<strong>هوشڌار:</strong> بٱلٛگاْ کاْآلشڌکاری اْکونین مٱتنؽ ڌاراْ کاْ مؽن ڤاست  مؽنتور ای نٱرمٱفزار ڤ کار رٱئڌاْ\nآلشڌ ای بٱلٛگاْ ڤانی آلشڌ بارت ڤاست مؽنتور ای نٱرم‌ٱفزار سی مؽنتورٱل داٛری اْڤۊهاْ",
+       "translateinterface": "سی ڤاروؤوٱن یا آلشڌنیڌن لرنیڌن سی گشڌ ڤیکی یٱل لوتفٱن زاْ[https://translatewiki.net/ translatewiki.net]، پروژاْ بۊمیسازی مڌیاڤیکی، اْسفاڌ کونین",
+       "cascadeprotected": "ای بٱلٛگاْ ؤور ؤوٱر آلشڌکاری پٱلڌاری آڤیڌاْ چیناکاْ مؽن {{PLURALبٱلٛگاْ|بٱلٛگیٱل}} پٱلڌاری آڤیڌاْ لٱم کاْ دزاْ «تافاوی» مؽن{{PLURAL:$1|هو|هونبٱل}} دزاْ آڤیڌاْ قرار گریڌاْ:\n$2",
+       "namespaceprotected": "اؽسا ٱجازاْ آلشڌکاری بٱلٛگاْیٱل فٱزای نوم '''$1''' ناْ نارین",
+       "customcssprotected": "اؽسا ٱجازاْ آلشڌکاری ای بٱلٛگاْ سی اس اس ناْ نارین، چیناکاْ هاڤی سامونکاری یٱ مؽنتور داٛری هؽڌآ",
+       "customjsonprotected": "اؽسا ٱجازاْ آلشڌکاری مؽن ای بٱلٛگاْء JSON ناْ نارین چیناکاْ سامونکاری سیخوڌی مؽنتورٱل ناْ داراْ",
+       "customjsprotected": "اؽسا ٱجازاْ آلشڌکاری ای بٱلٛگاْء جاڤااْسکریپت ناْ نارین چیناکاْ هاڤی سامونکاری سیخوڌی یٱ مؽنتور هٱنی هؽڌآ",
+       "sitecssprotected": "ایسا ساْلا آلشدکاری ای سی اٛس اٛس ناْ نارین سی یو کاْ گاشا بیننداْیٱل ریس کارگیٛرایی داشداْ بوݩ.",
+       "sitejsonprotected": "ایسا ساْلا آلشدکاری ای JSON ناْ نارین سی یو کاْ گاشا بیننداْیٱل ریس کارگیٛرایی داشداْ بوݩ.",
+       "sitejsprotected": "ایسا ساْلا آلشدکاری ای JavaScript ناْ نارین سی یو کاْ گاشا بیننداْیٱل ریس کارگیٛرایی داشداْ بۊن.",
+       "mycustomcssprotected": "ایسا ساْلا آلشدکاری css ای بٱلگاْ ناْ نارین",
+       "mycustomjsonprotected": "ایسا ساْلا آلشدکاری JSON ای بٱلگاْ ناْ نارین",
+       "mycustomjsprotected": "ایسا ساْلا آلشدکاری JavaScript ای بٱلگاْ ناْ نارین",
+       "myprivateinfoprotected": "ایسا سلا آلشدکاری دۊنسمٱندیٱل خوسۊسی خوتۊناْ نارین.",
+       "mypreferencesprotected": "ایسا سلا آلشدکاری چیا دیٱر خوتۊناْ نارین.",
+       "ns-specialprotected": "نیبۊ بٱلگاْیٱل ڤیجاْ ناْ آلشد کرد",
+       "titleprotected": "ای داسۊن ڤا دٱسدا کاریار [[User:$1|$1]] نیاگری ڤابیڌاْ.\nدلیلس یوناْ <em>$2</em>.",
+       "invalidtitle": "داسوݩ بی ٱرزشد",
+       "invalidtitle-knownnamespace": "داسوݩ نادیار سی نوم جا \"$2\" و متن \"$3\"",
+       "invalidtitle-unknownnamespace": "داسوݩ گٱن ڤا شوماراْ نومجا نادیار سی $1 و متن \"$2\"",
+       "exception-nologin": "هٱنی نٱڤۊڌیناْ ڤامیٛن",
+       "exception-nologin-text": "لوتف کونین بیائین ڤامیٛن سی یو کاْ سی ب ای بٱلگاْ یا کونشتکاری ریس دٱسرسی داشداْ بۊین.",
+       "exception-nologin-text-manual": "لوتف کونین ب $1 ساْلا بڌین کاْ ب ای بٱلگاْ یا کونشتکاری دٱسرسی داشداْ بۊ.",
+       "virus-badscanner": "سازڤاراْ گٱن:ڤیرۊس نادیار:<em>$1</em>",
        "virus-scanfailed": "ٱسکٱن جۊر نٱڤابی (code $1)",
-       "welcomeuser": "خۈش أڤوڌين،$1!",
-       "yourname": "نام کاربر:",
+       "virus-unknownscanner": "ڤیرۊس کوش نادیار:",
+       "logouttext": "<strong>ایسا هاْنی ناْڤۊڌیناْ ب دٱر.</strong>\n\nب ڤیرتۊ بۊ کاْ ڤیرگٱ نادیار ٱنجومیارتوݩ نٱ پاکسا کونین، سی یو کاْ گاشا پاراْیی ز بٱلگاْیٱل جۊری دیاری اْکونن میٛنیسدی ایسا ناْڤۊڌین ڤامیٛن.",
+       "cannotlogoutnow-title": "ایسا ایساْ نٱترین بیائین ب دٱر",
+       "cannotlogoutnow-text": "تا گاتی کاْ $1 ناْ ڤٱنین ڤا کار نٱترین بیائین ڤامیٛن.",
+       "welcomeuser": "خوش ٱڤوڌین،$1!",
+       "welcomecreation-msg": "هساوتوݩ راسد ڤابی.\nب ڤیرتوݩ بۊ کاْ {{نوم دیارگٱ}} [[Special:Preferences|preferences]]  خوتۊناْ آلشد کونین.",
+       "yourname": "نوم کاریاری:",
        "userlogin-yourname": "نوم کاریاری",
        "userlogin-yourname-ph": "نوم کاریاریتۊناْ بزنین",
-       "yourpassword": "رمز:",
-       "userlogin-yourpassword": "رازیناْ گوڤٱرتن",
-       "userlogin-yourpassword-ph": "رازیناْ گوڤٱرتن نٱ بزٱ",
-       "createacct-yourpassword-ph": "رازیناْ گوڤٱرتن نٱ بزٱ",
-       "createacct-yourpasswordagain": "پوشت راسدکاری رازینٱ گوڤٱرتن",
-       "createacct-yourpasswordagain-ph": "ز نۉ رازیناْ گوڤٱرتن نٱ بزٱ",
+       "createacct-another-username-ph": "نوم کاریاریتۊناْ بزنین",
+       "yourpassword": "رازیناْ گوڌٱشتن",
+       "userlogin-yourpassword": "رازیناْ گوڌٱشتن",
+       "userlogin-yourpassword-ph": "رازیناْ گوڌاْشتن ناْ بزٱ",
+       "createacct-yourpassword-ph": "رازیناْ گۉڌٱشتن ناْ بزٱ",
+       "yourpasswordagain": "رازیناْ گوڌٱشدن خوتۊناْ ز نۉ بزنین:",
+       "createacct-yourpasswordagain": "پوشت راسدکاری رازینٱ گوڌاْشتن",
+       "createacct-yourpasswordagain-ph": "ز نۉ رازیناْ گوڌاْشتن نٱ بزٱ",
        "userlogin-remembermypassword": "مۊناْ میٛن سامۊناْ ڤاڌار",
-       "cannotlogin-title": "نٱترین بیایین ڤامئن",
-       "yourdomainname": "پوشگر ايسا:",
-       "login": "ڤامین ٱڤوڌن",
-       "nav-login-createaccount": "اویدن به سیستم",
-       "logout": "رهدن زه سیستم",
-       "userlogout": "رهدن زه سیستم",
-       "userlogin-noaccount": "یٱ هساڤ کاریاری دارین؟",
+       "userlogin-signwithsecure": "ز رٱڤشت ٱمن ڤٱسل ڤابۊین",
+       "cannotlogin-title": "نٱترین بیایین ڤامیٛن",
+       "cannotlogin-text": "نٱترین بیائین ڤامیٛن",
+       "cannotloginnow-title": "ایسا ایساْ نٱترین بیائین ڤا میٛن",
+       "cannotloginnow-text": "تا گاتی کاْ $1 ناْ ڤٱنین ڤا کار نٱترین بیائین ڤامیٛن.",
+       "cannotcreateaccount-title": "نٱترین هساو کاریاری راسد کونین",
+       "cannotcreateaccount-text": "ایسا نٱترین موستٱقیم میٛن ای ڤیکی هساو کاریاری راسد کونین.",
+       "yourdomainname": "پۊشگر ايسا:",
+       "password-change-forbidden": "ایسا نٱترین رازیناْ گوڌٱشتن خوتۊناْ میٛن ای ڤیکی آلشد کونین.",
+       "externaldberror": "اٛشتڤایی میٛن پاٛیڤٱند ڤا رسیناْگا اْتفاق ڤٱستاْ یا ایسا ساْلا یوناْ کاْ یٱ هساو کاریاری خارجی ز خوتۊناْ ب هاْنگوم سازی کونین نارین.",
+       "login": "ڤامیٛن ٱڤوڌن",
+       "login-security": "نشۊن دیارکون خوتۊناْ آلشد کونین",
+       "nav-login-createaccount": "ٱڤوڌن ڤامیٛن/راس کردن هساو کاریاری",
+       "logout": "ز سامۊناْ درٱڤوڌن",
+       "userlogout": "ز سامۊناْ درٱڤوڌن",
+       "notloggedin": "هٱنی نٱڤۊڌیناْ ڤامیٛن",
+       "userlogin-noaccount": "یٱ هساو کاریاری دارین؟",
        "userlogin-joinproject": "ٱندوم دیارگٱ {{SITENAME}} ڤابۊین",
-       "createaccount": "راسد کردن هساڤ کاریاری",
+       "createaccount": "راسد کردن هساو کاریاری",
        "userlogin-resetpassword-link": "رازیناْ گوڤٱرتنتۊ ز ڤیرتۊن رٱهڌاْ؟",
        "userlogin-helplink2": "هومیاری کردن سی ڤامیٛن ٱڤوڌن",
-       "createacct-emailrequired": "تيرنشۈن أنجومانامأ",
+       "userlogin-loggedin": "ایسا ایساْ چی {{GENDER:$1|$1}} ٱڤۊڌین ڤامیٛن. فورم داْڤۊنی ناْ ڤٱنین ڤا کار و چی یٱ کاریار دیٱ بیائین ڤا میٛن",
+       "userlogin-createanother": "یٱ هساو کاریاری دیٱ راسد کونین",
+       "createacct-emailrequired": "تیرنشۊن ٱنجومانامٱ",
        "createacct-emailoptional": "تیرنشۊن ٱنجومانامٱ",
        "createacct-email-ph": "تیرنشۊن ٱنجوماناماْ تۊناْ بزنین.",
-       "createacct-another-email-ph": "تيرنشۈن أنجومانامأ تۈنأ بزنين",
+       "createacct-another-email-ph": "تیرنشۊن ٱنجوماناماْ تۊناْ بزنین.",
+       "createaccountmail": "یٱ رازیناْ گوڌٱشتن موڤٱقٱتی ناْ ڤاْنین ڤا کار و سی یٱ تیرنشوݩ ٱنجوماناماْ تیار ڤابیڌاْ باْسیس کونین.",
+       "createaccountmail-help": "ایسا ترین یٱ هساو کاریاری سی یکی دیٱ راسد کونین بی یو کاْ رازیناْ گوڌٱشتنساْ ڤٱنین ڤا ڤیر.",
+       "createacct-realname": "نوم راستٱکی(اٛژباری نی)",
        "createacct-reason": "دلیل",
-       "createacct-submit": "هساڤ خوتۊناْ راسد کونین",
-       "createacct-another-submit": "راسد کردن هساڤ کارياري",
+       "createacct-reason-ph": "سی چ ایسا دارین یٱ هساو کاریاری دیٱر راسد اْکونین",
+       "createacct-reason-help": "پاٛیغوم دیار کرداْ میٛن پاٛرستنوماْ راسد کردن هساو کاریاری",
+       "createacct-submit": "هساو خوتۊناْ راسد کونین",
+       "createacct-another-submit": "راسد کردن هساو کارياری",
+       "createacct-continue-submit": "هساو راسد کردن خوتۊناْ اٛڌاماْ بڌین",
+       "createacct-another-continue-submit": "هساڤ راسد کردن خوتۊناْ اٛڌاماْ بڌین",
        "createacct-benefit-heading": "{{SITENAME}}  ڤ دٱسد خٱلکی چی ایسا رٱڤٱندیاری ڤابیڌاْ.",
        "createacct-benefit-body1": "{{PLURAL:$1|آلشدکاری|آلشدکاریٱل}}",
        "createacct-benefit-body2": "{{PLURAL:$1|بٱلگاْ|بٱلگاْیٱل}}",
        "createacct-benefit-body3": "تازاْ{{PLURAL:$1|هوميار|هوميارٱل}}",
-       "loginerror": "خٱتا سی ڤامئن ٱڤوڌن",
-       "loginsuccesstitle": "اویدن با بخت وتوفیق به سیستم",
-       "loginsuccess": "''' ایسا اویدن به داخل سایت {{SITENAME}} بعنوان \"$1\".'''",
-       "nosuchuser": "کاربری به ای نام وجود نداره \"$1\".\nحروف نام را چک کنین, یا [[Special:CreateAccount|درست کنین یه حساب کاربری تازه]].",
-       "nosuchusershort": "کاربری به ای نام وجود نداره\"$1\".\nحروف نام راچک کنین.",
-       "nouserspecified": "ایسا دارین یه نام کاربر ذکر اکنین.",
-       "wrongpassword": "رمز وارد وابیده درست نه.\nلطفا دوباره سعی کنین.",
-       "wrongpasswordempty": "رمز وارد وابیده عقیم یامبهم بی.\nلطفا دوباره سعی کنین.",
-       "passwordtooshort": "رمز ایسا غیر معتبر یا کوتاه هده.\nآن وا داشته بوه حداقل {{PLURAL:$1|1 کاراکتر|$1 کاراکترها}} همچنین وا زه نام کاربریتو متفاوت بوه.",
-       "mailmypassword": "ز نۉ داڌن رازينإ گوأرتن",
-       "passwordremindertitle": "رمز موقتی تازه سی {{SITENAME}}",
-       "passwordremindertext": "یه نفر (شاید خودتو, زه نشانی آی پی$1) درخواست یه رمز تازه کرده سی {{SITENAME}} ($4). یه رمز موقتی سی کاربر\n\"$2\" درست شده وگذاشته وابیده به\"$3\". ایر مطابق میل ایسا بوه, نیازه که داخل سیستم بوین ویه رمز تازه انتخاب کنین.\n\nایر آن فرد همچنین درخواست کرده بوه  یونه, یا ایر ایسا رمزتو را به خاط داشته این ,\nوسی مدت طولانی نه خوین هونه تغییر بدین, ایسا وا نادیده بگیرین ای پیام  را وهمچنان زه رمز قدیمی خوتو استفاده کنین",
-       "noemail": "وجود نداره نشانی امیل ضبط وابده زه کاریر \"$1\".",
+       "badretype": "رازیناْ گوڌٱشتنی کاْ ایسا زاٛیڌیناْ هومبٱراڤٱر نیڌ.",
+       "usernameinprogress": "رٱڤٱندیاری یٱ هساو سی نوم کاریاری کاْ میٛن پیشکرداْ. یاْتی دٱس ڤاڌارین.",
+       "userexists": "نوم کاریاری کاْ داڌیناْ ایساْ ب کاراْ.\nلوتف کونین یٱ نوم دیٱ گولاْڤورچین کونین.",
+       "loginerror": "خٱتا سی ڤامیٛن ٱڤوڌن",
+       "createacct-error": "خٱتا راس کردن هساو کاریاری",
+       "createaccounterror": "نیبۊ هساو کاریاری راسد کونین:$1",
+       "nocookiesnew": "هساو کاریاری راست ڤابی، ڤٱلی ایسا هاْنی نٱڤوڌیناْ ڤامیٛن.{{SITENAME}} کۊکیا ناْ سی ڤامیٛن ٱڤوڌن ناْ کاریارٱل اْڤٱناْ ڤا کار.\nکۊکیا ایسا ناکونشتگٱر ڤابیڌناْ.\nلوتف کونین کونشتگٱرسون کونین، اوسو ڤا یٱ نوم کاریاری و رازیناْ گوڌٱشتن دیٱ بیائین ڤامیٛن.",
+       "nocookiesfornew": "هساو کاریاری راسد نٱڤابی، سی یو ناْ کاْ ایما نٱتریم سرچشماْساْ پوشت راست کاری کونیم.\nخاتر جٱم بۊین کاْ کۊکیٱل کونشتکار ڤابیناْ، ای بٱلگاْ ناْ ز نۉ سوڤار کونین و یٱ کاْرٱت دیٱ تلاش کونین.",
+       "loginsuccesstitle": "ایسا ٱڤوڌین ڤامیٛن",
+       "loginsuccess": "'''ایسا ٱڤوڌین ڤامیٛن {{SITENAME}} چی \"$1\".'''",
+       "nosuchuser": "چونو کاریاری ڤا نوم \"$1\" نیڌس.\nنوم کاریاری ب هٱرف کۊچیر و گٱپ هٱساساْ , یا [[Special:CreateAccount|یٱ هساو کاریاری دیاْ راسد کونین]].",
+       "nosuchusershort": "چونو کاریاری ڤا نوم \"$1\" نیڌس.\nرٱڤشت نڤشتن خوتۊناْ ڤارسی کونین.",
+       "nouserspecified": "ایسا ڤا یٱ نوم کاریاری تیار کونین.",
+       "login-userblocked": "کاریار نیاگری ڤابیڌاْ. سلا ٱڤوڌن ڤامیٛن ناراْ",
+       "wrongpassword": "رازیناْ گوڌاْشتنی کاْ زاٛیڌیناْ دوروست نیڌ\nمٱنمۊنداریم ز نۉ تلاش کونین.",
+       "wrongpasswordempty": "رازیناْ گوڌٱشتنتۊن هالی یا نادیار بی.\nمٱنمۊنداریم ز نۉ تلاش کونین.",
+       "passwordtooshort": "رازیناْ گوڌاْشدن ایسا ڤا هٱدٱقل {{PLURAL:$1|1 کاراکتر|$1 کاراکترٱل}} داشداْ بۊ.",
+       "passwordtoolong": "رازیناْ گوڌاْشدن ایسا نٱڤا  بیشتر ز {{PLURAL:$1|1 کاراکتر|$1 کاراکترٱل}} داشداْ بۊ.",
+       "password-name-match": "رازیناْ گوڌٱشتنتوݩ ڤا نوم کاریاری فٱرخ داشداْ بۊ",
+       "mailmypassword": "ز نۉ داڌن رازیناْ گوڌٱشتن",
+       "passwordremindertitle": "رازیناْ گوڌٱشتن موڤٱقٱتی سی {{SITENAME}}",
+       "passwordremindertext": "یٱ نفر (گاشا خوتوݩ، ز تیرنشوݩ آی پی $1) یٱ رازیناْ گوڌٱشتن تازاْ خاسداْ سی  {{SITENAME}} ($4). یٱ رازیناْ گوڌاْشتن موڤٱقٱتی سی کاریار\n\"$2\" راسد ڤابیڌاْ و میٛن\"$3\" لاهاڌاْ ڤابیڌاْ. ٱ ب دلتوݩ بۊ, ڤا رۉین میٛن ساموناْ و یٱ رازیناْ گوڌاْشتن تازاْ گولاْڤورچین کونین.\n\nٱر هو کٱسی کاْ چونو چی خاسداْ بۊ کاْس دیٱری بۊ, یا ٱر ایسا رازیناْ گوڌٱشتنتوݩ ب ڤیرتوݩ بۊ و سی یٱ گات تیلدار خاین هوناْ آلشد کونین، ایسا ڤا ای پاٛیغوم ناْ باْنین کنار و هٱمچونو هٱمو رازیناْ گوڌٱشتن دیندایی خوتوناْ ڤٱنین ڤا کار.",
+       "noemail": "هیژ تیرنشوݩ ٱنجوماناماْیی سی کاریار \"$1\" زٱفت نٱڤابیڌاْ.",
        "passwordsent": "یه رمز تازه ارسال وابید به نشانی امیل ثبت وابده سی \"$1\".\nلطفا بعد از دریافت آن داخل سیستم بوین.",
-       "eauthentsent": "Û\8cÙ\87 Ø§Û\8cÙ\85Û\8cÙ\84 Ø³Û\8c ØªØ§Û\8cÛ\8cد Ø¢Ø¯Ø±Ø³ Ø§Û\8cÙ\85Û\8cÙ\84 Ø¨Ù\87 Ø¢Ø¯Ø±Ø³ Ù\85Ù\88رÙ\86ظر Ø§Ø±Ø³Ø§Ù\84 Ù\88ابÛ\8cد. Ù\82بÙ\84 Ø²Ù\87 Û\8cÙ\88 Ú©Ù\87 Ø§Û\8cÙ\85Û\8cÙ\84 Ø¯Û\8cگرÛ\8c Ù\82ابÙ\84 Ø§Ø±Ø³Ø§Ù\84 Ø¨Ù\87 Ø§Û\8cÙ\86 Ø¢Ø¯Ø±Ø³ Ø¨Ù\88Ù\87Ø\8c Ù\88ا Ø¯Ø³ØªÙ\88رÙ\87اÛ\8cÛ\8c Ú©Ù\87 Ø¯Ø± Ø¢Ù\86 Ø§Û\8cÙ\85Û\8cÙ\84 Ø§Ù\88Û\8cدÙ\87 Ø±Ø§ Ø¬Ù\87ت ØªØ£Û\8cÛ\8cد Ø§Û\8c Ù\85ساÙ\84Ù\87 Ú©Ù\87 Ø§Û\8c Ø¢Ø¯Ø±Ø³ Ù\85اÙ\84 Ø§Û\8cساÙ\86Ù\87 Ø§Ø¬Ø±Ø§ Ú©Ù\86Û\8cÙ\86.",
-       "emaildisabled": "اي ديارگأ نترإ إنجومانامإ سيتۈن بفرشنإ",
-       "accountcreated": "هساڤ راسد ڤابي",
+       "eauthentsent": "Û\8cÙ± Ù±Ù\86جÙ\88Ù\85اÙ\86اÙ\85اÙ\92 Ù¾Ù\88شت Ø±Ø§Ø³Øª Ú©Ø±Ø¯Ù\86Û\8c Ø³Û\8c Û\8cÙ± ØªÛ\8cرÙ\86Ø´Ù\88Ý© Ú¤Û\8cجاÙ\92 Ø¨Û\8cÙ\9bسÛ\8c Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92.\nÙ\86Û\8cا Û\8cÙ\88 Ú©Ø§Ù\92 Û\8cÙ± Ù±Ù\86جÙ\88Ù\85اÙ\86اÙ\85اÙ\92 Ø¯Û\8cٱر Ø³Û\8c Ù\87ساÙ\88تÙ\88Ý© Ø¨Û\8cÙ\9bسÛ\8c Ú¤Ø§Ø¨Û\8aØ\8c Ø§Û\8cسا Ú¤Ø§ Ù\86Û\8cا Ø±Ù±Ø¯Û\8cارکÙ\88Ù\86Û\8c Ù\86اÙ\92 Ø² Ù±Ù\86جÙ\88Ù\85اÙ\86اÙ\85اÙ\92 Ø¨Ú¯Ø±Û\8cÙ\86Ø\8c Ø³Û\8c Û\8cÙ\88 Ú©Ø§Ù\92 Ù\87ساÙ\88 Ø§Û\8cسا Ø² Ø±Ø§Ø³ØªÛ\8c Ù¾Ù\88شت Ø±Ø§Ø³Øª Ú¤Ø§Ø¨Û\8a.",
+       "emaildisabled": "ای دیارگٱ نٱتٱراْ سیتوݩ ٱنجوماناماْ بفرشناْ",
+       "accountcreated": "هساو راسد ڤابی",
        "createaccount-title": "هساڤ سي {{SITENAME}} راسد ڤابي",
        "loginlanguagelabel": "زڤون:$1",
        "pt-login": "ڤامین ٱڤوڌن",
        "pt-login-button": "ڤامیٛن ٱڤوڌن",
-       "pt-createaccount": "راسد کردن هساڤ کاریاری",
+       "pt-createaccount": "راسد کردن هساو کاریاری",
        "pt-userlogout": "ز سامۊناْ درٱڤوڌن",
        "changepassword": "آلشد کردن رازينإ گوڤأرتن",
+       "resetpass_header": "رازیناْ گوڌاْشتن هساو ناْ آلشد کونین",
        "oldpassword": "رازينإ گوڤأرتن ديندایي:",
        "newpassword": "رازينإ گوڤأرتن تازأ:",
        "retypenew": "تایپ دوباره رمز:",
        "passwordreset-username": "نوم کارياري",
        "passwordreset-domain": "پوشگر",
        "passwordreset-email": "تيرنشۈن أنجومانامأ",
-       "passwordreset-emailtitle": "جوزيات هساڤ مإن{{SITENAME}}",
+       "passwordreset-emailtitle": "جوزيات هساو میٛن {{SITENAME}}",
        "passwordreset-invalidemail": "تيرنشۈن أنجومانامأ نادوروسد",
        "changeemail-oldemail": "تيرنشۈن أنجومانامإ ايسني",
        "changeemail-newemail": "تيرنشۈن أنجومانامإ تازأ:",
        "changeemail-password": "رازينإ گوڤأرتن {{SITENAME}} ایسا:",
        "changeemail-submit": "آلشد کردن أنجومانامأ",
        "resettokens": "ز نۉ کردن نشۈنإیل",
-       "resettokens-text": "اؽسا تٱرین شناساننداٛیٱلؽ کاْ اجازاٛ دٱسرٱسی ڤاْ قٱرڌؽ داداٛیٱل سیخؤاٛی مؽنڌار ڤا هساڤتان ناْ اْڌاْ دوکرتشناسی کونین.\nؤخڌؽ ڤا ای کارناْ ٱنجوم ڤڌین کاْ تٱساڌوفٱن هونوناْ ڤا کسؽ ڤاْ هومبٱشنی نهاڌین یا کسؽ ڤاْمؽ ڤیڌ ڤاْ هساڤ اؽسا",
+       "resettokens-text": "اؽسا تٱرین شناساننداٛیٱلؽ کاْ اجازاٛ دٱسرٱسی ڤاْ قٱرڌؽ داداٛیٱل سیخؤاٛی مؽنڌار ڤا هساوتۊن ناْ اْڌاْ دوکرتشناسی کونین.\nؤخڌؽ ڤا ای کارناْ ٱنجوم ڤڌین کاْ تٱساڌوفٱن هونوناْ ڤا کسؽ ڤاْ هومبٱشنی نهاڌین یا کسؽ ڤاْمؽ ڤیڌ ڤاْ هساو اؽسا",
        "resettokens-no-tokens": "هیچ شناسانٱنڌاٛئی سی دوکرتشناسی نؽڌا",
        "resettokens-tokens": "نشۈنإیل:",
        "resettokens-token-label": "$1 (أرزایشت تازأ: $2)",
        "publishpage": "تیژنیڌن بٱلٛگاْ",
        "publishchanges": "تیژنیڌن آلشڌٱل",
        "savearticle-start": "کۊنیڌن بٱلٛگاْ",
-       "preview": "پیش نمایش",
+       "savechanges-start": "کۊکرڌن آلشدکاریٱل",
+       "publishpage-start": "تیژنیڌن بٱلٛگاْ....",
+       "publishchanges-start": "تیژنیڌن آلشڌکاریٱل",
+       "preview": "پيش ساٛیل",
        "showpreview": "نشۊن دائن پیش ساٛیل",
        "showdiff": "نشۊن دائن آلشدا",
-       "anoneditwarning": "<strong>ب ڤیرتۊن بۊ:</strong> ایسا هاْنی نٱڤۊڌین ڤامین. تیرنشۊن آی پی ایسا سی هر گاتی کاْ آلشدکاری کونین سی کول خٱلک دیاراْ. ٱر <strong>[$1 رۉین ڤامین]</strong> یا <strong>[$2 یٱ هساڤ کاریاری راسد کونین]</strong>، آلشدکاریٱل ایسا ڤا نوم کاریاری خوتۊن دیاری اْبۊ و یو سی ایسا بیتراْ.",
-       "summary-preview": "پیش نمایش - خلاصه:",
-       "blockedtext": " \"'''دسترسی نام کاربری یا نشانی اینترنتی ایسا بسته وابیده.'''\nای کار توسط $1 انجام شده‌است.\nدلیلی که گده اینه: $2''\n* آغاز قطع دسترسی: $8\n* زمان اتمام ای قطع دسترسی: $6\n* کاربری که قطع دسترسی‌اش در نظر بیده: $7\nایساترین با $1 یا یکی از [[{{MediaWiki:Grouppage-sysop}}|مدیران]] تماس بگیرین و در ای باره صحبت کنین.\nتوجه کنین که ایسا نترین زه امکان «ارسال پست الکترونیکی به ای کاربر» استفاده کنین مگر این که نشانی پست الکترونیکی معتبری در [[Special:Preferences|اولویتهای کاربری]]خود ثبت کرده بوین.\nنشانی IP ایسا $3 و شماره قطع دسترسی ایسا $5 است. لطفاً ای شماره‌ها را در همه کاوشهاتون ذکر کنین.\nایسا ترین با $1 یا یکی دیه زه [[{{MediaWiki:Grouppage-sysop}}|مدیران]] تماس بگیرین، تا در باره ای قطع دسترسی صحبت کنین.\nدقت کنین که سی ارسال پست الکترونیکی در ویکی، وا پست الکترونیکی خود را زه طریق صفحه[[Special:Preferences|تنظیمات]] فعال کرده بوین، و نیز، وا امکان استفاده زه ای ویژگی سی ایساقطع نبوه.\nنشانی اینترنتی الان ایسا $3 است و شماره قطع دسترسی $5 است.\nلطفاً ای شماره را در هر درخواستی که در ای مورد مطرح اکنین ذکر کنین",
+       "anoneditwarning": "<strong>ب ڤیرتوݩ بۊ:</strong> ایسا هاْنی نٱڤۊڌین ڤامین. تیرنشوݩ آی پی ایسا سی هر گاتی کاْ آلشدکاری کونین سی کول خٱلک دیاراْ. ٱر <strong>[$1 رۉین ڤامین]</strong> یا <strong>[$2 یٱ هساو کاریاری راسد کونین]</strong>، آلشدکاریٱل ایسا ڤا نوم کاریاری خوتوݩ دیاری اْبۊ و یو سی ایسا بیتراْ.",
+       "summary-preview": "پیش ساٛیل آلشدکاری خولاساْ:",
+       "blockedtext": " \"'''دٱسرسی نوم کاریاری یا تیرنشوݩ آی پی ایسا نیاگری ڤابیڌاْ.'''\n $1 چونو کرداْ.\nدلیلس یو بیڌاْ: $2''\n* شورۊ نیاگری: $8\n* مجال تٱموم ڤابیڌن نیاگری: $6\n* کاریاری کاْ ڤا نیاگری ڤابیڌاْ بۊ: $7\nایسا تاْرین ڤا $1 یا یکی ز [[{{MediaWiki:Grouppage-sysop}}|سٱردیڤۊنکاروݩ]] تماس بگرین و ڤاسوݩ گٱپ بزنین.\nب ڤیرتوݩ بۊ کاْ ایسا ناْترن «ب ای کاریار ٱنجوماناماْ» بفرشنین مٱر تیرنشوݩ جادیاری ناْ میٛن  [[Special:Preferences|چیا ٱسلی کاریاری]] خوتوݩ سٱبت کرداْ بۊین.\nتیرنشوݩ IP ایسا $3 و شوماراْ نیاگری ڤابیڌاْ ایسا $5 اْ. لوتفٱن چونو شوماراْ یٱلی ناْ میٛن پاٛی جۊریٱل توݩ ب ڤیرتوݩ بۊ.",
+       "blockednoreason": "هیژ دلیلی سیس نی",
+       "nosuchsectiontitle": "بٱئرجا دیاری نیکوناْ",
        "loginreqtitle": "ڤامإن إڤوڌن لازومإ",
        "loginreqlink": "ڤامین ٱڤوڌن",
        "accmailtitle": "رازينإ گوڤأرتن فرشناڌإ ڤابيڌإ",
        "newarticletext": "ایسا ز دین یٱ هومپاٛیڤٱندی هڌین کاْ نیڌس. سی رٱڤٱندیاری بٱلگاْ شورۊ کونین میٛن ای جٱڤاْ داٛڤۊنی بنڤیسین(سی دۊنسدن بیشدر سئیل [$1]کونین).\nیر ایسا سی اْشتڤاکارش ايچونین، دوگماْ رٱهڌن ڤاپوشد نٱ بپۊرنین.",
        "noarticletext": " ایساْ ای بٱلگاْ نڤشداْیی ناراْ، ایسا تاْرین [[Special:Search/{{PAGENAME}}داسۊن ای بٱلگاْ نٱ میٛن بٱلگاْیٱل دیٱری پاٛی جۊری کونین]] یا [{{fullurl:{{FULLPAGENAME}}|action=edit}} ای بٱلگاْ نٱ آلشدکاری کونين].",
        "noarticletext-nopermission": " ایساْ ای بٱلگاْ نڤشداْیی ناراْ، ایسا تاْرین [[Special:Search/{{PAGENAME}}داسۊن ای بٱلگاْ نٱ میٛن بٱلگاْیٱل دیٱری پاٛی جۊری کونین]] یا [{{fullurl:{{FULLPAGENAME}}|action=edit}} ای بٱلگاْ نٱ آلشد کونين].",
-       "previewnote": "'''ای فقط یه پیش نمایشه;\nتغییراتی که ایسا دادین هنی ضبط نوابیده!'''",
-       "editing": "درحال اصلاح $1",
+       "userpage-userdoesnotexist-view": "هساو کاریاری \"$1\" سٱبت نٱڤابیڌاْ.",
+       "updated": "(ب هاْنگوم سازی ڤابیڌاْ)",
+       "note": "<strong>میراس:</strong>",
+       "previewnote": "'''ب ڤیرتۊن بۊ یو یٱ پیش ساٛیلاْ;\nآلشدکاریٱلی کاْ ایسا هاْنی اْمایاْسۊن نکردیناْ!'''",
+       "continue-editing": "رۉ مئن راستاگاْ آلشدکاری",
+       "editing": "ب هال و بال آلشدکاری $1",
        "creating": "راسد کردن $1",
        "editingsection": "ب هال و بال آلشدکاری $1 (بٱرجا)",
        "yourtext": "متن ايسا",
+       "storedversion": "ڤانیٱری کۊ ڤابیڌاْ",
+       "yourdiff": "فرخ",
        "copyrightwarning": "لطفاً دقت کنین که درنظر گریده ابوه که همه شراکتهای ایسا  {{SITENAME}} تحت «$2» منتشر ابون ).\n\n\n(سی دیدن  جزئیات بیشتر به $1 برین\n\nایر نه خوین نوشته‌هاتو بی‌رحمانه اصلاح بوه و به دلخواه ارسال بوه، ایچو نفرستن.<br />\nدرضمن ایسادارین به ایما قول ادین که خودتو یونه نوشتین یا هونه زه یک منبع آزاد با مالکیت عمومی یا مثل هو ورداشتین. '''کارهای دارای کارهای دارای حق کپی رایت را بی‌اجازه نفرستین!'''',",
        "templatesused": "{{PLURAL:$1|چۊاْ|چۊاْیٱل}} ڤا کار ڤٱسداْ میٛن ای بٱلگاْ:",
        "templatesusedpreview": "قالڤٱل یا اولگۊیٱل ڤاْ کار رٱئڌاْ مؽن ای نهانماو",
        "hiddencategories": "ای بٱلگاْ یکی ز ٱندوما {{PLURAL:$1|1 hidden category|$1 hidden categories}} اْ:",
        "nocreatetext": "{{SITENAME}}قابلیت درست کردن صفحات تازه را محدود کرده‌. ترین برگردین و صفحه‌ موجود را اصلاح کنین یا اینکه  [[Special:UserLogin|به سیستم داخل بوین یا حساب کاربری درست کنین]].",
        "permissionserrors": "پٱلاْ:اْجازاْ ڤگرا",
-       "permissionserrorstext-withaction": "ايسا سی نياگري $2 سإلا\nنارين {{PLURAL:$1|دلیل|دليلا}}:",
+       "permissionserrorstext-withaction": "ایسا سی نیاگری $2 ساٛلا\nنارین {{PLURAL:$1|دلیل|دلیلٱل}}:",
        "recreate-moveddeleted-warn": "'''هوشڌار: ایسا هنؽ سٱرزاْنۊ بٱلٛگی ناْ ؤرکل اْکونین کاْ نهاتٱرپاکسا آڤیڌاْ '''مؽن فرگ داشڌ ڤۊهین کاْ آلشڌ ای بٱلٛگاْ کارؽ دوروساْ هؽڌا آ نٱ. نمایاٛ پاکسا  مؽنڌار ڤا ای بٱلٛگاْ سی راهٱتی کار ڤا دینا آؤوڌاْ",
        "moveddeleted-notice": "ای بٱلٛیاْ پاکسا آڤیڌاْ،ؤرداوناْ سیاهؽ پاکسا،هناڌاری ۉ کلٛ کرڌن ای بٱلٛیاْ ؤرتی نهاڌ آڤیڌاْ",
+       "edit-conflict": "ری ب ری کاری میٛن ڤیرایشت.",
+       "slot-name-main": "سرتال",
        "content-model-wikitext": "ڤيکي تکست",
        "content-model-javascript": "جاڤا إسکريپت",
+       "content-json-empty-object": "داسۊن هالی",
+       "content-json-empty-array": "آرایاْ هالی",
        "undo-failure": "سی نڤیڌن سلۊکی ڤا آلشڌکاریٱل مؽنجخائی ای آلشڌکاریناْ نؽڤۊ بؽ هرنڳ کرڌ",
        "viewpagelogs": "دیاری کردن پهرستنۊماْیٱل ای بٱلگاْ",
        "currentrev": "نسخه جاری",
        "currentrev-asof": "آخرین ڤانیٱری جۊر $1",
        "revisionasof": "ڤانیٱری چی $1",
-       "revision-info": "Ù\86Ù\88سÙ\82Ø¥ Ú¤Ø§Ù\86Ù\8aأرÙ\8a Ú¤Ø§Ø¨Ù\8aÚ\8cØ¥ Ø¬Û\88ر $1 ڤا $2",
+       "revision-info": "Ù\86Ù\88سÙ\82اÙ\92 Ú¤Ø§Ù\86Û\8cٱرÛ\8c Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92 Ø¬Û\8aر $1 ڤا $2",
        "previousrevision": "← ڤانیٱری دیندایی",
        "nextrevision": "نوسقاْ نیایی →",
        "currentrevisionlink": "آخری ڤانیٱری",
        "last": "دیندایی",
        "page_first": "اولین",
        "page_last": "آخری",
-       "histlegend": "انتخاب متفاوت: علامت بنین رو رادیو جعبه ها زه آن نسخه ها سی مقایسه وامتیاز دادن ویا داخل تکمه های زیر سی مسابقه بوین  .<br />\n'شرح: (فعلی) = تفاوت با نسخه جاری\n(قبلی) = تفاوت با نسخه قبلی، جز = ویرایش جزئی',",
-       "history-fieldset-title": "پاٛی جۊری سی ۋانیاٛریا",
-       "histfirst": "دينداتري",
-       "histlast": "تازإترين",
+       "histlegend": "گولاْڤورچین کردن سٱڤا: رو رادیۉ جٱڤاْیٱل ڤانیٱری نشۊن باْنین سی کنار یٱک ناهاڌن و اْمتیاز داڌن و یا میٛن دوگماْیٱل ناْ سی موساڤقاْ بپۊرنین .<br />\n<br />\nمیرات: <strong>({{int:cur}})</strong> = فٱرخ ڤا نوسقاْ ایسنی, <strong>({{int:last}})</strong> =فٱرخ ڤا نوسقاْ دیندایی, <strong>{{int:minoreditletter}}</strong> = هیرداْ ڤیرایشت.",
+       "history-fieldset-title": "پاٛی جۊری سی ڤانیٱریٱل",
+       "histfirst": "دینداتری",
+       "histlast": "تازاْترين",
+       "historysize": "({{PLURAL:$1|1 بایت|$1 بایتٱل}})",
        "historyempty": "(هالي)",
        "history-feed-title": "ؤیرگارکاْ آلشڌکاریٱل",
        "history-feed-description": "ؤرگارکاْ آلشڌکاریٱل ای بٱلٛگاْ مؽن ڤیکی",
-       "history-feed-item-nocomment": "$1 در $2",
+       "history-feed-item-nocomment": "$1 میٛن $2",
        "rev-delundel": "آلشد هال و بال ديڌن",
        "rev-showdeleted": "دياري کردن",
        "revdelete-show-file-submit": "هأرإ",
+       "revdelete-radio-set": "قام آبیڌاْ",
+       "revdelete-radio-unset": "دٱم تی",
        "revdelete-log": "دلیل:",
        "mergehistory-from": "بألگإ سرچشمأ:",
        "mergehistory-reason": "دلیل:",
        "history-title": "دوڤارتاْ دیڌن ڤیرگار $1",
        "difference-title": "فٱرخ میٛنجقا ڤاناٛیریا \"$1\"",
        "lineno": "خٱت $1:",
-       "compareselectedversions": "مقایسه نسخه‌های انتخاب‌ وابیده",
+       "compareselectedversions": "کنار یٱک ناهاڌن ڤانیٱریٱل گولاْڤورچین ڤابیڌاْ",
        "editundo": "ٱنجومشیڤ کردن",
+       "diff-empty": "(یٱ جۊر)",
        "diff-multi-sameuser": "({{PLURAL:$1|یٱ دۊناٛ نوسقاٛ مؽنجخایی|$1 نوسقاٛیٱل مؽنجخایی}} ب دٱسد{{PLURAL:$2|کاریاری ديٱ|$2 کاريارا}} نشۊن دیاری نٱکرداْ)",
+       "diff-multi-otherusers": "({{PLURAL:$1|یٱ نوسقاْ میٛنجقایی|$1 نوسقاْیٱل میٛنجایی}} ڤا دٱسد {{PLURAL:$2|کاریاری دیٱ|$2 کاریارٱل}} نشۊن داڌاْ نٱڤابیڌاْ)",
        "searchresults": "نتيجاْیٱل پاٛی جۊری",
        "searchresults-title": "نتيجاْیٱل پاٛی جۊری سی \"$1\"",
        "prevn": "دیندایی {{PLURAL:$1|$1}}",
        "nextn": "نیایی {{PLURAL:$1|$1}}",
+       "next-page": "بٱلگاْ نيايی",
+       "prevn-title": "دیندایی $1 {{PLURAL:$1|نتيجٱ|نتیجاْیٱل}}",
        "nextn-title": "نیایی $1 {{PLURAL:$1|نتيجٱ|نتیجاْیٱل}}",
        "shown-title": "نشۊن دائن $1 {{PLURAL:$1|نتیجاْ|نتیجاْیٱل}} سی هر بٱلگٱ",
        "viewprevnext": "دیئن ($1 {{int:pipe-separator}} $2) ($3)",
+       "searchmenu-exists": "<strong>ایچو میٛن ای ڤیکی یٱ بٱلگاْ هڌ کاْاْسمس \"[[:$1]]\" اْ </strong> {{PLURAL:$2|0=|هٱمچونو ساٛیل نتیجاْیٱلی کاْ دیار کرداْ ناْ کونین.}}",
        "searchmenu-new": "<strong>اي بٱلگاْ نٱ میٛن \"[[:$1]]\" ای ڤیکی راسد کو!</strong> {{PLURAL:$2|0=|See also the page found with your search.|See also the search results found.}}",
        "searchprofile-articles": "بٱلگاْیٱل مینۊناْ دار",
        "searchprofile-images": "مۊلتی مدیا",
        "search-result-category-size": "\n{{PLURAL:$1|یٱهومڤٱن|$1 هومڤٱن}} ({{PLURAL:$2|یٱزؽر بنکۊ|$2 زؽر بنکۊ}}، {{PLURAL:$3|یٱ پٱرڤٱناْ|$3 پٱرڤٱناْ}})",
        "search-redirect": "(ڤاگٱردۊنی ز $1)",
        "search-section": "(بٱرجا $1)",
+       "search-category": "(دٱسداْ $1)",
+       "search-file-match": "(یکی کردن مینۊناْ جانیا)",
        "search-suggest": "مٱنزۊرت یو بی:$1",
        "search-interwiki-more": "(بيشدر)",
        "searchall": "هٱمٱ",
        "search-showingresults": "{{PLURAL:$4|نتیجاْیٱل<strong>$1</strong> ز <strong>$3</strong>|نتیجاْ یٱل<strong>$1 - $2</strong ز <strong>$3</strong>}}",
        "search-nonefound": "هیژ نتیجاْیی ڤا پاٛی جوست تۊن یکی نیڌ.",
+       "powersearch-togglelabel": "ڤارسی کردن:",
        "powersearch-toggleall": "همأ",
        "powersearch-togglenone": "هيش کوم",
        "preferences": "اولویتها",
        "group-bot": "روڤاتٱل",
        "group-sysop": "سٱردیڤۊنکارۊن",
        "grouppage-bot": "{{ns:project}}:بوتا",
-       "grouppage-sysop": "{{ns:project}}:مدیران",
+       "grouppage-sysop": "{{ns:project}}:سٱردیڤۊنکاریۊن",
        "right-writeapi": "سي نڤشدن اْی پی آی ڤٱنين ڤاکار",
+       "grant-createaccount": "راسد کردن هساو کاریاری",
        "newuserlogpage": "راسد ڤابیاْ ڤا کاریار",
-       "rightslog": "Ù\86Ù\85اÛ\8cÙ\87 Ø­Ù\82Ù\88Ù\82 Ú©Ø§Ø±Ø¨ر",
-       "action-edit": "ای بلگٱ نٱ ۋیرایشد کو",
-       "action-createaccount": "ڤاکل ای هساْڤ مؽنتوری",
+       "rightslog": "Ù¾Ù\87رستÙ\86Û\8aÙ\85اÙ\92 Ø­Ù\82Ù\88Ù\82 Ú©Ø§Ø±Û\8cار",
+       "action-edit": "ای بلگٱ نٱ آلشدکاری کو",
+       "action-createaccount": "ڤاکل ای هساْو مؽنتوری",
        "nchanges": "$1 {{PLURAL:$1|تغییر|تغییرات}}",
        "enhancedrc-history": "ڤیرگار",
        "recentchanges": "آلشدکاریا ایسنی",
        "recentchanges-legend": "گوزیناْیٱل آلشدکاریٱل ایسنی",
        "recentchanges-summary": "دو بیشتر آلشدٱل تازاْ باڤ ڤیکی ناْ ز ای بٱلگاْ پاٛیگری کو.",
-       "recentchanges-feed-description": "ردیابی آخرین تغییرات  ویکی در ای خورد",
+       "recentchanges-noresult": "هیژ آلشدکاری میٛن گات ای چیا اْتفاق نٱڤٱسداْ",
+       "recentchanges-feed-description": "بیشتر آلشتکاریٱل تازاْ میٛن ڤیکی نٱ کاْ میٛن هڤال خۊنن پاٛیگری کو.",
        "recentchanges-label-newpage": "ای آلشدکای یٱ بٱلگاْ تازاْ راسد کرداْ",
        "recentchanges-label-minor": "یو یٱ ڤیرایشد کۊچیراْ",
        "recentchanges-label-bot": "ای ڤیرایشتاْ نٱ یٱ بوت ٱنجوم دائاْ",
        "recentchanges-label-plusminus": "ٱندازاْ بٱلگاْ ب شومار ای بایتٱل آلشد کرداْ.",
        "recentchanges-legend-heading": "<strong>میراس:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (هٱچونوݩ ڤاْ[[Special:NewPages|نومگٱ بٱلٛگیٱل نۊ]] ساٛل ؤوٱنین)",
-       "rcnotefrom": "ۋازیر {{PLURAL:$5|ۋیرایشد|ۋیرایشدا}}ز ۋیرگار strong>$3, $4</strong> تا ۋیرگار <strong>$1</strong>  دیاری اٛکونن .",
+       "rcnotefrom": "ڤازیر {{PLURAL:$5|آلشدکاری|آلشدکاریٱل}}ز ڤیرگار strong>$3, $4</strong> تا ڤیرگار <strong>$1</strong>  دیاری اْکونن .",
        "rclistfrom": "دیار کردن آلشدکاریٱل ز $3 $2",
        "rcshowhideminor": "آلشدکاری کۊچیر $1",
-       "rcshowhideminor-show": "نشون دائن",
+       "rcshowhideminor-show": "نشۊن دائن",
        "rcshowhideminor-hide": "قام کردن",
        "rcshowhidebots": "$1 بوتا",
        "rcshowhidebots-show": "نشۊن دائن",
        "rcshowhideliu-show": "دیار کردن",
        "rcshowhideliu-hide": "قام کردن",
        "rcshowhideanons": "$1 کاریارٱل ڤامیٛن نٱڤوڌاْ",
-       "rcshowhideanons-show": "نشون دائن",
+       "rcshowhideanons-show": "نشۊن دائن",
        "rcshowhideanons-hide": "قام کردن",
-       "rcshowhidepatr": "$1 Ø§ØµÙ\84احات Ù¾Ø§Ø³Ø¯Ø§Ø±Û\8c Ø´Ø¯Ù\87",
+       "rcshowhidepatr": "$1 Ø¢Ù\84شدکارÛ\8cÙ±Ù\84 ØªÛ\8cڤاÚ\8cاشت Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92",
        "rcshowhidemine": "$1 آلشدکاریا مو",
-       "rcshowhidemine-show": "نشون دائن",
+       "rcshowhidemine-show": "نشۊن دائن",
        "rcshowhidemine-hide": "قام کردن",
        "rclinks": "دیاری کردن دینائی $1 آلشڌ مؽن $2 رۊز دیندایی",
        "diff": "فٱرخ",
        "newpageletter": "ن",
        "boteditletter": "ب",
        "rc-change-size-new": "$1 {{PLURAL:$1|بایت|بایتا}} نیا آلشدکاری",
+       "rc-old-title": "زاتٱ چی \"$1\" راس ڤابیڌاْ",
        "recentchangeslinked": "آلشدکاریٱل تاْ یٱک",
        "recentchangeslinked-feed": "تغییرات مرتبط",
        "recentchangeslinked-toolbox": "آلشدکاریٱل تاْ یٱک",
        "recentchangeslinked-to": "آلشدکاریٱلی کاْ میٛن بٱلگاْیٱل هومپاٛیڤٱند بیناْ ب جا بٱلگاْ داڌاْ ڤابیڌاْ دیاریسۊن کو",
        "upload": "سوڤار کردن جانیا",
        "uploadbtn": "آپلود فایل",
-       "uploadlogpage": "Ù\86Ù\85اÛ\8cÙ\87 Ø¢Ù¾Ù\84Ù\88د",
+       "uploadlogpage": "Ù¾Ù\87رستÙ\86Û\8aÙ\85اÙ\92 Ø³Ù\88ڤارکرد",
        "filedesc": "چكستٱ",
-       "license": "ماٛن حال و بال لیسانس دار ۋابیاٛن",
+       "license": "میٛن هال و بال لیسانس دار ڤابیڌن",
        "license-header": "میٛن هال و بال ليسانس دار ڤابيڌن",
        "imgfile": "جانیا",
-       "listfiles": "Ù\84Û\8cست Ù\81اÛ\8cÙ\84",
+       "listfiles": "Ù\86Ù\88Ù\85گاÙ\92 Ø¬Ø§Ù\86Û\8cا",
        "file-anchor-link": "جانیا",
        "filehist": "ڤیرگار جانیا",
        "filehist-help": "ری ڤیرگار بپۊرنین تا نوسقاْیی کاْ خاین باْنیٱرین",
        "filehist-comment": "ڤیر و باڤٱر",
        "imagelinks": "ڤا کار ڤٱندن جانیا",
        "linkstoimage": "پیشٱر گوهڌاْ ڤابیڌاْ {{PLURAL:$1|ڤا کار ڤٱندن بٱلگاْیٱل|$1 ڤا کار ڤٱندن بٱگاْیٱل }} سی ای جانیا:",
+       "linkstoimage-more": "More than $1 {{PLURAL:$1|page uses|pages use}} this file.\nThe following list shows the {{PLURAL:$1|first page|first $1 pages}} that use this file only.\nA [[Special:WhatLinksHere/$2|full list]] is available.",
        "nolinkstoimage": "ای پٱرڤٱناْ مؽن هیچ بٱلٛیاْ نؽڌا",
+       "linkstoimage-redirect": "$1 (ڤاگٱردۊنی جانیا) $2",
        "sharedupload": "ای فایل یک آپلود اشتراکی هده و ممکنه زه طریق  پروژه‌های دیگه  هم قابل دسترسی بوه",
        "sharedupload-desc-here": "جانیایی کاْ میٛن $1 گاشا میٛن پوروجٱیٱل هٱنی ٱم ب کار گرهڌاْ ڤابیڌاْ بۊ.\nتۉزی سی [$2 file description page] میٛن دڤۊن دیاراْ",
+       "filepage-nofile": "چونو جانیایی ڤا چونڤ اْسمی نیڌس.",
        "uploadnewversion-linktext": "آپلود کردن یه نسخه تازه زه ای فایل",
        "upload-disallowed-here": "ايسا ناْترین ای جانیا نٱ ز نۉ سوڤار کونین",
        "mimesearch": "MIME جستجو رو پایه",
        "fewestrevisions": "صفحات با کمترین تعداداصلاحات وتجدیدنظرها",
        "nbytes": "$1 {{PLURAL:$1|بایت|بایت}}",
        "nlinks": "$1 {{PLURAL:$1|لینک|لینکها}}",
-       "nmembers": "$1 {{PLURAL:$1|عضو|اعضا}}",
+       "nmembers": "$1 {{PLURAL:$1|ٱندوم|ٱندوما}}",
        "lonelypages": "صفحات یتیم وابیده",
        "uncategorizedpages": "صفحات دسته بندی نوابیده",
        "uncategorizedcategories": "دسته های دسته بندی نوابیده",
        "longpages": "صفحات بلند",
        "deadendpages": "صفحات بن بست ولاینحل",
        "protectedpages": "صفحات حفاظت وحمایت وابیده",
-       "listusers": "Ù\84Û\8cست Ú©Ø§Ø±Ø¨ر",
+       "listusers": "Ù\86Ù\88Ù\85گاÙ\92 Ú©Ø§Ø±Û\8cار",
        "newpages": "بٱلگاْیٱل نۏ",
        "ancientpages": "کهنه ترین صفحات",
        "move": "جابجا کردن",
        "movethispage": "جابجایی ای صفحه",
-       "pager-newer-n": "{{PLURAL:ۋانیاتر ز 1|ۋانیاتر ز $1}}",
-       "pager-older-n": "{{PLURAL:$1|گپساÙ\84تر 1|Ú¯پسالتر $1}}",
+       "pager-newer-n": "{{PLURAL:ڤانیاتر ز 1|ڤانیاتر ز $1}}",
+       "pager-older-n": "{{PLURAL:$1|گٱپساÙ\84تر 1|Ú¯Ù±پسالتر $1}}",
        "booksources": "سرچشماْیٱل کتاو",
        "booksources-search-legend": "پاٛ جۊری سی سٱرچٱشمٱیٱل کتاو",
        "booksources-search": "پاٛی جۊری",
        "specialloguserlabel": "مؽنتور:",
-       "speciallogtitlelabel": "اÙ\88Ú\8cڤاÙ\86:",
+       "speciallogtitlelabel": "داÙ\84(داسÛ\8aÙ\86 Û\8cا {{ns:user}}:Ù\86Ù\88Ù\85 Ú©Ø§Ø±Û\8cارÛ\8c Ø³Û\8c Ú©Ø§Ø±Û\8cار):",
        "log": "پاْرستنوماْیٱل",
        "all-logs-page": "گشڌنمائیٱل",
-       "allpages": "همه صفحات",
+       "alllogstext": "نمایشت یٱ جا کاْ تٱموم پهرستنۊماْیٱل میٛن {{SITENAME}}.\nایسا تاْرین ڤا گولاْڤورچین کردن جۊر پهرستنۊماْ ، نوم کاریاری(هساس ب کۊچیری و گٱپی هٱرفا) و بٱلگاْیٱل آلشت کرداْ(هساس ب گٱپی و کۊچیری هٱرفا) نمایشت نٱ دیر ز ڤیر کونین.",
+       "logempty": "چونو چی کاْ ایسا خاسدین میٛن پهرستنۊماْ نیڌس",
+       "allpages": "تٱموم بٱلگاْیٱل",
        "nextpage": "صفحه بعدی ($1)",
        "prevpage": "صفحه قبلی($1)",
        "allpagesfrom": "نمایش دادن صفحات با شروع زه:",
        "allpagesprefix": "نشو دادن صفحات همراه با پیشوند:",
        "allpages-hide-redirects": "بؽ دیارنیڌن آلشڌتورٱل",
        "categories": "دٱسداْیٱل",
-       "emailuser": "امیل ای کاربر",
+       "listgrouprights-members": "(نومگاْ ٱندومٱل)",
+       "emailuser": "ٱنجوماناماْ کاریار",
+       "usermessage-editor": "پاٛیغوم فرشن سامۊناْیی",
        "watchlist": "لیسڌ دیناگریٱل مو",
        "mywatchlist": "سئیل بٱرگ",
        "watchlistfor2": "سی $1 $2",
        "deleteotherreason": "دیه/دلیل اضافی:",
        "deletereasonotherlist": "دلیل دیه",
        "rollbacklink": "ڤورگٱشتن",
-       "rollbacklinkcount": "چڤاسإ کردن $1 {{PLURAL:$1|ویرایشت|ویرایشتیا}}",
-       "protectlogpage": "Ù\86Ù\85اÛ\8cÙ\87 Ø­Ù\81اظت Ù\88Ø­Ù\85اÛ\8cت",
+       "rollbacklinkcount": "چٱڤاساْ کردن $1 {{PLURAL:$1|آلشدکاری|آلشدکاریٱل}}",
+       "protectlogpage": "Ù¾Ù\87رستÙ\86Û\8aÙ\85اÙ\92 Ù¾Ù±Ø± Ù\88 Ù¾Û\8cÙ\85 Ú©Ø§Ø±Û\8c",
        "protectedarticle": "پٱر و پیم ڤابیڌاٛ \"[[$1]]\"",
        "modifiedarticleprotection": "بارت هناگری«[[$1]]» ناْ آلشڌنیڌ",
        "prot_1movedto2": "[[$1]] جابجا وابید به[[$2]]",
        "protect-text": "ایسا ممکنه بوینین وتغییر بدین سطح حمایت زه ای صفحه'''$1'''.",
        "protect-locked-access": "حساب کاربری ایسا اجازه تغییر سطح حمایت ای صفحه را نداره.\nای چونه تنظیمات جاری سی آن صفحه '''$1''':",
        "protect-cascadeon": "ای صفحه  در حال حاضر حفاظت وحمایت وابیده چون که در {{PLURAL:$1|صفحه|صفحات}}\nزیر که گزینه حفاظت وحمایت موجی {{PLURAL:$1|آن|آن‌ها}} فعال هده ،\nایسا ترین سطح حفاظت ای صفحه را تغییر بدین اما ای کارنتره تاثیری رو\nحفاظت وحمایت موجی صفحه داشته بوه.",
-       "protect-default": "Ù\87Ù\85اÙ\9b Ú©Ø§Ø±Û\8cارا سلادارن",
+       "protect-default": "Ù\87Ù±Ù\85اÙ\92 Ú©Ø§Ø±Û\8cارٱÙ\84 سلادارن",
        "protect-fallback": "درخواست\"$1\" اجازه",
        "protect-level-autoconfirmed": "بستن کاربران ثبت نام نوابیده",
        "protect-level-sysop": "Sysops فقط",
        "tooltip-namespace_association": "ای جٱڤاْ نٱ ڤارسی کونین نأ ڤارسي کونين، ای جٱڤاْ  ز ڤٱرگرهڌاْ چٱک چناْ یٱل داسۊن نوم ڤٱرگٱ شریکی و نوم ڤٱرگٱ گولاْڤورچیناْ.",
        "blanknamespace": "(ٱسلی)",
        "contributions": "{{GENDER:$1|کاریار}} هومیاریٱل",
-       "contributions-title": "هومیاری كاریارآ سی $1",
+       "contributions-title": "هومیاری کاریارٱل سی $1",
        "mycontris": "هومياریٱل",
        "anoncontribs": "هومياریٱل",
        "contribsub2": "سی {{GENDER:$3|$1}} ($2)",
        "nocontribs": "هیچ آلشڌؽ ڤا ای دیاریٱل جوساْ نڤیڌ",
-       "uctop": "تازاÙ\9b Ø¨Ø§Û\8b",
+       "uctop": "تازاÙ\92 Ø¨Ø§Ù\88",
        "month": "ز ای ما (دینداترس):",
        "year": "ز ٱمسال (و سال دینداتری):",
+       "sp-contributions-newbies": "فٱقٱت هومیاریٱلی کاْ ز هساڤٱل تاز بیڌناْ دیاری کو.",
        "sp-contributions-newbies-sub": "سی حسابهای کاربری تازه",
-       "sp-contributions-blocklog": "Ù\86Ù\85اÛ\8cÙ\87 Ø¨Ø³ØªÙ\87 Ù\88ابÛ\8cدÙ\87 Ù\87ا",
-       "sp-contributions-uploads": "سوڤارکردا",
-       "sp-contributions-logs": "Ù\86Û\8bشدÙ\86 Ø±Ù\88Ø® Û\8bÙ\86دا",
+       "sp-contributions-blocklog": "Ù¾Ù\87رستÙ\86Ù\88Ù\85اÙ\92 Ù\82Ù\88Ù\84Ù\81 Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92",
+       "sp-contributions-uploads": "سوڤارکردٱل",
+       "sp-contributions-logs": "پاÙ\92رستÙ\86Û\8aÙ\85اÙ\92Û\8cÙ±Ù\84",
        "sp-contributions-talk": "چٱک چناٛ",
-       "sp-contributions-search": "سی هومیاریا پاٛی جۊری ۋابۊ",
+       "sp-contributions-search": "سی هومیاریٱل پاٛی جۊری ڤابۊ",
        "sp-contributions-username": "نوم ناٛشۊن آی پی یا نوم کاریاری",
+       "sp-contributions-toponly": "فقٱت آلشدکاریٱلی کاْ جۏزڤاْ آخریݩ دۉران دیاری کو",
+       "sp-contributions-newonly": "فٱقٱت آلشدکاریٱلی نٱ کاْ میٛن گات راست کردن بٱلگاْ بیڌناْ دیاری کو.",
        "sp-contributions-submit": "پاٛی جۊری",
        "whatlinkshere": "کوم هومپاٛیندٱل ایچونن",
        "whatlinkshere-title": "بٱلگاْیٱل هومپاٛیڤٱند ڤابیناْ ب \"$1\"",
        "whatlinkshere-page": "بٱلگاْ",
        "linkshere": "چونو بٱلگاْیٱلی هومپاٛیڤٱند ڤابیناْ ب '''$2''':",
-       "nolinkshere": "Ù\87Û\8cÚ\86 ØµÙ\81Ø­Ù\87 Ø§Û\8c Ù¾Û\8cÙ\88Ù\86د Ù\86داردبÙ\87 '''$2'''.",
+       "nolinkshere": "Ù\87Û\8cÚ\98 Ø¨Ù±Ù\84گاÙ\92 Ø¨ '''$2''' Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÛ\8cÚ¤Ù±Ù\86د Ù\86اراÙ\92.",
        "isredirect": "بٱلگاْ ڤاگٱردۊنی",
        "istemplate": "ڤارو گونجایشدٱل",
        "isimage": "جانیا هومپاٛیڤٱند",
        "whatlinkshere-hideredirs": "$1 ڤاگٱردۊنی",
        "whatlinkshere-hidetrans": "ڤارو گونجایشدٱل $1",
        "whatlinkshere-hidelinks": "هومپاٛیڤٱند سی $1",
+       "whatlinkshere-hideimages": "جانیا هومپاٛیڤٱندٱل $1",
        "whatlinkshere-filters": "فيلترٱل",
        "blockip": "بستن کاربر",
-       "ipboptions": "۲ ساعت:2 hours,۱ روز:1 day,۳ روز:3 days,۱ هفته:1 week,۲ هفته:2 weeks,۱ ماه:1 month,۳ ماه:3 months,۶ ماه:6 months,۱ سال:1 year,بی‌نهایت:infinite",
+       "ipbcreateaccount": "راسد کردن هساو کاریاری",
+       "ipboptions": "۲ سات:2 hours,۱ رۊز:1 day,۳ رۊز:3 days,۱ هٱفتاْ:1 week,۲ هٱفتاْ:2 weeks,۱ ما:1 month,۳ ما:3 months,۶ ما:6 months,۱ سال:1 year,بی ڤیرگار:infinite",
        "ipblocklist": "آدرسهای  آی پی وکاربران بسته وابیدند",
+       "blocklist-userblocks": "قام کردن هساو نیاگری ڤابیڌاْ.",
        "infiniteblock": "بؽ تٱ",
+       "createaccountblock": "هساو راسد کردن ناکونشتگٱر ڤابیڌ",
+       "blocklist-editing-page": "بٱگاْیٱل",
        "blocklink": "نیاگری ڤابۊ !",
        "unblocklink": "باز بوه",
        "contribslink": "هٱیاری",
-       "blocklogpage": "Ù\86Ù\85اÛ\8cÙ\87 _ Ø¨Ø³ØªÙ\87â\80\8cÙ\88ابÛ\8cدÙ\87â\80\8cÙ\87ا",
-       "blocklogentry": "بسته وابید [[$1]] با سپری وابیدن وقت زه $2 $3",
+       "blocklogpage": "Ù¾Ù\87رستÙ\86Ù\88Ù\85اÙ\92 Ù\82Ù\88Ù\84Ù\81 Ú¤Ø§Ø¨Û\8cÚ\8cاÙ\92",
+       "blocklogentry": "قولف ڤابی [[$1]] ڤا خلاس ڤابیڌن گات ز $2 $3",
        "reblock-logentry": "ساموکارٱل بورسناْ دٱسرٱسی [[$1]]آلشڌ ڤاْ تٱهاْ بورسناْ دٱسرٱسی مؽن $2 $3",
-       "block-log-flags-nocreate": "هرنڳ ؤرکلنیڌن هساڤ بؽ هرنڳ آڤیڌ",
+       "block-log-flags-nocreate": "هرنڳ ؤرکلنیڌن هساو بؽ هرنڳ آڤیڌ",
        "proxyblocker": "ؤٱرڤٱناْ پروکسی",
        "movepagetext": "با استفاده زه فرم زیر نام صفحه تغییر اکنه و همه گزارش تاریخی هو به نام تازه جابجا ابوه.\nعنوان کهنه تبدیل به یک صفحه تغییر مسیر به عنوان جدید ابوه.\nایسا ترین بطور اتوماتیک تغییر مسیر های مربوط به عنوان اصلی رو به روز رسانی کنین. ایر ایسا مطمئن نهدین با دیدن یونو مطمئن بوین:\n[[Special:تغییر مسیر دوبل|دوبل ]] یا[[Special:تغییرمسیر خروا یا اشکسته|تغییرمسیرهای خراو یا اشکسته]].\n\nلینکهایی که به عنوان صفحه قدیمی هدن تغییر نه کنن حتماً تغییر مسیرهای دوبل یا اشکسته و خراو را بررسی کنین.\n'''ایسا''' مسئول اطمینان زه یو هدین که لینکها هنی به همان‌جایی که قرار هده برن.\n\nتوجه کنین که ایر زه قبل صفحه‌ای در عنوان تازه وجود داشته بوه صفحه منتقل '''نه بوه'''،\nمیر یو که آن صفحه خالی یا تغییر مسیر بوه و گزارش تاریخی اصلاح نداشته بوه.\n یعنی ایر اشتباه کردین ترین صفحه را به همان جایی که زه هو جابجا وابیدین برگردانین، و  نترین رو صفحات موجود بنویسین\n\n'''هشدار!'''\nجابجایی صفحات به نام تازه ممکنه  تغییر کلی و غیرمنتظره‌ای سی\n صفحات دوست داشتنی داشته بوه ؛\nلطفاً مطمئن بوین که قبل زه جابجا کردن صفحه، عواقب ای کار را درک اکنین.",
        "movepagetalktext": "صفحه صحبت مربوط، ایر وجود داشته بوه، بطور اتوماتیک همراه با صفحه اصلی\n جابجا ابوه '''میر یو که''' :\n* در حال جابجایی صفحه زه ای فضای نام به فضای نام دیگری بوین،\n* یه صفحه صحبت غیرخالی تحت ای نام تازه وجود داشته بوه، یا\n* کادر زیر را تیک نزده بوین.\n\nدر ای موارد، وا صفحه را بطور دستی جابجا کرده و یا محتویات دو صفحه را با اصلاح ادغام کنین.",
        "tooltip-pt-mycontris": "یٱ نومگٱ ز هومیاریٱل {{GENDER:|ایسا}}",
        "tooltip-pt-login": "ایما اٛگۊیم کاْ رۉین ڤامین سامۊنگٱ: ڤاْلی چونو کاری اٛژباری نی",
        "tooltip-pt-logout": "ز سامۊناْ درٱڤوڌن",
-       "tooltip-pt-createaccount": "ايسا پوشت گٱرم ڤابیڌیناْ کاْ یٱ هساڤ راسد کونین و بیائین ڤامین. ڤاْلی چونو کاری اٛژباری نی.",
+       "tooltip-pt-createaccount": "ايسا پوشت گٱرم ڤابیڌیناْ کاْ یٱ هساو راسد کونین و بیائین ڤامین. ڤاْلی چونو کاری اٛژباری نی.",
        "tooltip-ca-talk": "چٱک چناْ سی مینۊناْ بٱلگٱ",
        "tooltip-ca-edit": "ای بٱلگاْ نٱ آلشدکاری کو",
        "tooltip-ca-addsection": "شورۊ کردن یٱ بٱرجا دیٱ",
        "tooltip-ca-viewsource": "ای بٱلگاْ پٱر و پیم ڤابیڌاْ.\nایسا تاْرین سرچشماْساْ بڤنین",
        "tooltip-ca-history": "دڤوارتاْ دیئن ای بٱلگٱ",
-       "tooltip-ca-protect": "Ø­Ù\81اظت Ù\88Ø­Ù\85اÛ\8cت Ø²Ù\87 Ø§Û\8c ØµÙ\81Ø­Ù\87",
-       "tooltip-ca-delete": "حذÙ\81 Ø§Û\8c ØµÙ\81Ø­Ù\87",
+       "tooltip-ca-protect": "اÛ\8c Ø¨Ù±Ù\84گاÙ\92 Ù\86اÙ\92 Ù¾Ù±Ø± Ù\88 Ù¾Û\8cÙ\85 Ú©Ù\88",
+       "tooltip-ca-delete": "اÛ\8c Ø¨Ù±Ù\84گاÙ\92 Ù\86اÙ\92 Ù¾Ø§Ú©Ø³Ø§ Ú©Ù\88",
        "tooltip-ca-move": "جابجا کردن ای بٱلگاْ",
        "tooltip-ca-watch": "اْزاف کردن ای بٱلگٱ ب سئیل بٱرگ خوتۊن",
-       "tooltip-ca-unwatch": "حذف ای صفحه زه لیست پی‌گیری‌های ایسا",
+       "tooltip-ca-unwatch": "ڤورداشتن بٱلگاْ ز ساٛیل بٱرگتۊن",
        "tooltip-search": "جوستن {{SITENAME}}",
        "tooltip-search-go": "رۉ میٛن بٱلگاْیٱلی کاْ نوم راستٱکی داشتاْ بۊن",
        "tooltip-search-fulltext": "بٱلگاْیٱل نٱ سی چونو نڤشداْیٱلی پاٛی جۊری کو",
        "tooltip-ca-nstab-special": "ای بٱلگاْ یٱ بٱلگاْ ڤیجاْ، و نیبۊ آلشدکاریس کونین",
        "tooltip-ca-nstab-project": "دیڌن بٱلگاْ پوروجاْ",
        "tooltip-ca-nstab-image": "دیئن بٱلگاْ جانیا",
-       "tooltip-ca-nstab-mediawiki": "دیدن پیام سیستم",
+       "tooltip-ca-nstab-mediawiki": "دیڌن پاٛیغوم سامۊناْ",
        "tooltip-ca-nstab-template": "دیڌن چۊاْ",
        "tooltip-ca-nstab-help": "دیدن صفحه کمک",
        "tooltip-ca-nstab-category": "دیئن بٱلگاْ دٱسداْ بٱندی",
-       "tooltip-minoredit": "علامت نهادن به این به عنوان اصلاح حداقل یاکم",
+       "tooltip-minoredit": "یوناْ چی یٱ هیرداْ ڤیرایشت نشۊن دیار کو",
        "tooltip-save": "اْمایاْ کردن آلشدکاریتۊن",
        "tooltip-preview": "پیش سئیل آلشدکاریٱلتۊن، لوتف کونین یۊنونٱ دیندا اْمایاْ کردن ڤٱنین ڤا کار.",
        "tooltip-diff": "دیئن آلشدکاریٱلی کاْ ایسا میٛن ای متن راست کردیناْ.",
-       "tooltip-compareselectedversions": "دیدن تفاوتهای بین دونسخه انتخاب وابیده زه ای صفحه",
-       "tooltip-watch": "اضاف کردن ای صفحه به فهرست مشاهداتتان",
+       "tooltip-compareselectedversions": "دیڌن فٱرخ میٛنجقا دوتا نوسقاْ گولاْڤورچین ڤابیڌاْ ز ای بٱلگاْ.",
+       "tooltip-watch": "اْزاف کردن ای بٱلگٱ ب ساٛیل بٱرگ خوتۊن",
        "tooltip-recreate": "دوباره درست کردن صفحه ای که زه رو کینه وغرض پاک وابیده",
        "tooltip-upload": "شروع آپلود",
        "tooltip-rollback": "\"ڤورگٱنیئن\" لرنیڌن سی هال و بال ٱڤٱل ای بٱلگٱ سی یو کاْ هومیاری نیایی سی بیتٱر کردن بۊ ٱلڤٱت ڤا یٱ کرٱت پۊرنیڌن.",
        "pageinfo-robot-policy": "فاٛئرست کرڌن ڤا روڤات",
        "pageinfo-robot-index": "موجاز",
        "pageinfo-robot-noindex": "بؽ اعتڤار",
-       "pageinfo-watchers": "Ø´Ù\88Ù\85اراÙ\9b دیناگروناٛ بٱلٛگاْ",
+       "pageinfo-watchers": "Ø´Ù\88Ù\85اراÙ\92 دیناگروناٛ بٱلٛگاْ",
        "pageinfo-few-watchers": " ساوا تٱر زاْ $1 {{PLURAL:$1| دیناگری|دیناگری}}",
        "pageinfo-redirects-name": "بور آلشڌ لۊرٱل ڤاْ ای بٱلٛگاْ",
        "pageinfo-subpages-name": "زؽر بٱلٛگیٱل ای بٱلٛگاْ",
        "pageinfo-subpages-value": "1 ($2 {{PLURAL:$2|آلشڌتور|آلشڌ تور}}; $3 {{PLURAL:$3|خاٛراز آلشڌ تور|خاٛراز آلشڌ تور}})",
        "pageinfo-firstuser": "بٱلگاْ راس کون",
+       "pageinfo-firsttime": "گات دروس ڤابیڌن بٱلگاْ",
        "pageinfo-lastuser": "آخری ڤیرایشتکار",
        "pageinfo-lasttime": "گات آخری ڤیرایشت",
+       "pageinfo-edits": "کول آلشدکاریٱل",
+       "pageinfo-authors": "شوماراْ کولی نڤیسیارٱل یٱکۊنٱ",
+       "pageinfo-recent-edits": "شوماراْ آلشدکاریٱل ایسنی (د $1 دینداتر)",
+       "pageinfo-recent-authors": "شوماراْ کولی نڤیسیارٱل یٱکۊنٱ",
        "pageinfo-magic-words": "{{PLURAL:$1|قساْ|قساْ}} جادۊیی ($1)",
        "pageinfo-hidden-categories": "{{PLURAL:$1| بنکۊ|بنکۊ}} بؽ دیار ( $1 )",
        "pageinfo-templates": "{{PLURAL:$1|اولگۊیٱل|اولگۊیٱل}} اْسفاڌاْ آڤیڌاْ ($1)",
        "pageinfo-toolboxlink": "ڌونائیٱل بٱلٛگاْ",
        "pageinfo-contentpage": "باٛئنٱت آڤیڌاْ ڤاْ اوڌڤان بٱلٛگاْ موهتٱڤائی",
        "pageinfo-contentpage-yes": "هٱراٛ",
-       "patrol-log-page": "سیاهاْ لاٛر",
+       "patrol-log-page": "پهرستنۊماْ لاٛر",
        "previousdiff": "← آلشدکاری دیندایی",
        "nextdiff": "آلشدکاری تازاْتر→",
+       "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|بلگاْ|بلگاْیٱل}}",
        "file-info-size": "$1 × $2 پیکسل, ٱندازاْ جانیا: $3, MIME جۊر: $4",
        "file-info-size-pages": "<span style=\"direction:ltr\">$1 × $2</span> نوخڌاْ، مٱشلٱقی پٱرڤٱناْ: $3، نوع MIME پٱرڤٱناْ: $4، $5 بٱلٛگاْ",
        "file-nohires": "ٱسگ بیتری زس نی",
        "imgmultigo": "رۉڤا",
        "imgmultigoto": "رٱئڌن ڤاٛ بٱلٛگاْ $1",
        "watchlisttools-clear": "پاکسانیڌن فاٛئرسڌ دیناگری",
-       "watchlisttools-view": "نشودادن تغییرات مربوطه",
-       "watchlisttools-edit": "نشودادن واصلاح کردن لیست پیگیریها",
-       "watchlisttools-raw": "اصÙ\84اح Ù\84Û\8cست Ø®Ø§Ù\85 Ù¾Û\8câ\80\8cÚ¯Û\8cرÛ\8câ\80\8cÙ\87ا",
+       "watchlisttools-view": "ساٛیل کردن آلشدکاریٱل چی یٱک",
+       "watchlisttools-edit": "دیڌن و آلشدکاری کردن ساٛیل بٱرگ",
+       "watchlisttools-raw": "Ø¢Ù\84شدکارÛ\8c Ø³Ø§Ù\9bÛ\8cÙ\84 Ø¨Ù±Ø±Ú¯",
        "signature": "[[{{ns:user}}:$1|$2]] ([[{{ns:user_talk}}:$1|چٱک چنٱ]])",
        "version": "ترجمه یا تفسیر",
        "redirect": "آلشد لۊر ڤا پٱرڤٱناْ،مؽنتور،بٱلٛگاْ دبارنوماْ نوسخاْ",
        "redirect-submit": "رۉ",
+       "redirect-lookup": "پاٛی جۊری",
        "redirect-value": "ٱرزایشت:",
        "redirect-user": "نوم دیارکون کاریار:",
        "redirect-page": "دیارکار بٱلٛگاْ",
+       "redirect-revision": "ڤانیٱری بٱلگاْ",
        "redirect-file": "نوم جانیا",
        "specialpages": "بٱلگاْیٱل ڤیجٱ",
        "tag-filter": "[[Special:سٱرڌیسٱل|سٱرديس]]فیلتر:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|سرديس|سرديسا}}]]: $2",
        "tags-active-yes": "هٱراٛ",
        "tags-active-no": "نٱ",
-       "tags-hitcount": "$1 {{PLURAL:$1|Ø¢Ù\84شت|Ø¢Ù\84شتا}}",
+       "tags-hitcount": "$1 {{PLURAL:$1|Ø¢Ù\84شدکارÛ\8c|Ø¢Ù\84شدکارÛ\8cÙ±Ù\84}}",
        "logentry-delete-delete": "$1 بٱلگاْ {{GENDER:$2|پاکسا ڤابيڌاْ}} $3",
        "logentry-delete-restore": "$1{{GENDER:$2|آلشڌ کرڌن}} بٱلٛگاْ ناْ$3سی$4",
        "logentry-delete-revision": "$1 دیاری {{PLURAL:$5|یٱ نوسخاْ|$5 نوسخاْ}} بٱلٛگاْ $3 ناْ{{GENDER:$2|آلشڌکرڌ}}: $4",
        "revdelete-content-hid": "هؽلنیڌناْ بؽڌیارکرڌ",
        "logentry-move-move": "$1 {{GENDER:$2|جا ب جا کرداْ}} بٱلگاْ $3 نٱ سی $4",
+       "logentry-move-move-noredirect": "$1 {{GENDER:$2|جا ب جا کرد}} بٱلگاْ $3 نٱ سی $4 بی یو یٱ ڤاگٱردۊنی داشداْ بۊ",
        "logentry-move-move_redir": "$1 بٱلٛگاْ $3 ناْ ڤاْ $4 کاْ آلشڌ تور ڤیڌاْ {{GENDER:$2|کل کرڌ}}",
        "logentry-patrol-patrol-auto": "$1 نوسخهٔ $4 بٱلٛگاْ $3 ناْ خودکار ڤاْ عنڤان لرخوؤٱرداٛ {{GENDER:$2|دزاْ کونین}}",
        "logentry-newusers-create": "هساڤ کاریاری $1 {{GENDER:$2|راسد ڤابی}}",
-       "logentry-newusers-autocreate": "حساڤ $1  ڤاْ بارت خوڌکار {{GENDER:$2|ؤرکل آڤیڌاْ}}",
-       "logentry-upload-upload": "$1 {{GENDER:$2|سوڤار کرده}} $3",
+       "logentry-newusers-autocreate": "هساو $1  ڤاْ بارت خوڌکار {{GENDER:$2|ؤرکل آڤیڌاْ}}",
+       "logentry-upload-upload": "$1 {{GENDER:$2|سوڤار کرداْ}} $3",
+       "logentry-upload-overwrite": "$1 یٱ نوسقاْ تازاْ ز $3 ناْ {{GENDER:$2|سوڤار کرد}}",
        "searchsuggest-search": "جوستن {{SITENAME}}",
-       "duration-days": "$1 رۊز",
-       "randomrootpage": "بٱلٛگاْ بونی تٱساڌفی"
+       "duration-days": "$1 {{PLURAL:$1|رۊز|رۊزٱل}}",
+       "randomrootpage": "بٱلٛگاْ بونی تٱساڌفی",
+       "log-action-filter-all": "هٱمٱ",
+       "log-action-filter-block-block": "نیاگری ڤابۊ",
+       "log-action-filter-block-unblock": "نیاگری نٱڤابۊ",
+       "log-action-filter-delete-delete": "پاکسا کردن بٱلگاْ",
+       "authmanager-authplugin-setpass-bad-domain": "پۊشگر نادیار.",
+       "authmanager-retype-help": "سی پوشت راسدکاری ز نۉ رازیناْ گوڌٱشتن ناْ بزنین",
+       "authmanager-email-label": "ٱنجوماناماْ",
+       "authmanager-email-help": "تیرنشۊن ٱنجوماناماْ",
+       "authmanager-realname-label": "نوم راستاْکی",
+       "authprovider-resetpass-skip-label": "خولاساْ",
+       "authform-newtoken": "تۊکن باْلا ڤابیڌاْ. $1",
+       "authform-notoken": "تۊکن باْلا ڤابیڌاْ",
+       "authform-wrongtoken": "تۊکن خراو",
+       "specialpage-securitylevel-not-allowed-title": "ساْلا چونو کاری ناْ نارین",
+       "authpage-cannot-login": "نیبۊ بیائین ڤامیٛن.",
+       "credentialsform-account": "نوم هساو:",
+       "cannotlink-no-provider-title": "هیژ هساو هومپاٛیڤٱنداری نی.",
+       "linkaccounts": "هومپاٛیڤٱند هساڤٱل",
+       "linkaccounts-submit": "هومپاٛیڤٱند هساڤٱل",
+       "unlinkaccounts-success": "هساو کاریاری ناهومپاٛیڤٱند ڤابی.",
+       "edit-error-short": "خٱتا: $1",
+       "edit-error-long": "خٱتایٱل:$1",
+       "revid": "ڤانیٱری $1"
 }
index 72da302..e2af414 100644 (file)
        "ipb-sitewide": "A tot el lloc web",
        "ipb-partial": "Parcial",
        "ipb-pages-label": "Pàgines",
+       "ipb-namespaces-label": "Espais de noms",
        "badipaddress": "L'adreça IP no té el format correcte.",
        "blockipsuccesssub": "S'ha blocat amb èxit",
        "blockipsuccesstext": "S'ha {{GENDER:$1|blocat}} [[Special:Contributions/$1|$1]].<br />\nVegeu la [[Special:BlockList|llista de blocatges]] per revisar-los.",
        "blocklist-nousertalk": "no podeu modificar la pàgina de discussió pròpia",
        "blocklist-editing": "edició",
        "blocklist-editing-sitewide": "edició (a tot el lloc)",
+       "blocklist-editing-page": "pàgines",
+       "blocklist-editing-ns": "espais de noms",
        "ipblocklist-empty": "La llista de blocatges està buida.",
        "ipblocklist-no-results": "L'adreça IP o nom d'usuari sol·licitat no està blocat.",
        "blocklink": "bloca",
index fac44a6..9a4d0d2 100644 (file)
        "recentchangescount": "Ӏадйитаран кепаца гойтуш долу нисдарийн дукхалла",
        "prefs-help-recentchangescount": "Гойту керла нисдарш, агӀонийн истори, тептарш.",
        "prefs-help-watchlist-token2": "Иза хьан тергаме могӀан къайла догӀа ду.\nМуьлха и хуучунна йиш ю хьан тергаме могӀам беша, цундела ма хаийта иза кхечаьрга. [[Special:ResetTokens|ТӀетаӀа йе кхуза и хьайга кхосса лууш делахь]].",
+       "prefs-help-tokenmanagement": "Хьа йиш ю хьажина хьайн дӀаяздаран доӀа кхосса. Оцо аьтту бо хьа тергаме могӀаман веб-каналан тӀекхача. И доӀа хуучун йиш ю хьа тергаме могӀаме хьажа, цундела ма хаийта иза кхечаьрга.",
        "savedprefs": "Хьан гӀирс Ӏалашбина.",
        "savedrights": "{{GENDER:$1|$1}} декъашхочун бакъонаш Ӏалашйина.",
        "timezonelegend": "Сахьтан аса:",
        "userrights-expiry": "Бакъо дӀайолу:",
        "userrights-expiry-existing": "$2, $3",
        "userrights-expiry-othertime": "Кхин хан:",
-       "userrights-expiry-options": "1 де:1 day,1 кӀира:1 week,1 бутт:1 mopnth,3 months,6 бутт:6 months,1 шо:1 year",
+       "userrights-expiry-options": "1 де:1 day,1 кӀира:1 week,1 бутт:1 month,3 months,6 бутт:6 months,1 шо:1 year",
        "userrights-invalid-expiry": "«$1» группин хан чеккхйолу хан нийса яц.",
        "group": "Тоба:",
        "group-user": "Декъашхой",
index 68ff6fa..9b12df8 100644 (file)
        "blocklist-nousertalk": "nemůže editovat svou diskusní stránku",
        "blocklist-editing": "editování",
        "blocklist-editing-sitewide": "editování (na celém projektu)",
+       "blocklist-editing-page": "stránky",
+       "blocklist-editing-ns": "jmenné prostory",
        "ipblocklist-empty": "Seznam probíhajících bloků je prázdný.",
        "ipblocklist-no-results": "Požadovaná IP adresa nebo uživatelské jméno není blokováno.",
        "blocklink": "zablokovat",
index 3e9a600..a79c51c 100644 (file)
        "watchnologin": "Ikke logget på",
        "addwatch": "Tilføj til overvågningsliste",
        "addedwatchtext": "\"[[:$1]]\" og tilhørende diskussionsside er blevet tilføjet til din [[Special:Watchlist|overvågningsliste]].",
+       "addedwatchtext-talk": "\"[[:$1]]\" og dens tilhørende side er blevet føjet til din [[Special:Watchlist|overvågningsliste]].",
        "addedwatchtext-short": "Siden \"$1\" er blevet tilføjet til din overvågningsliste.",
        "removewatch": "Fjern fra overvågningsliste",
        "removedwatchtext": "\"[[:$1]]\" og tilhørende diskussionsside er blevet fjernet fra din [[Special:Watchlist|overvågningsliste]].",
+       "removedwatchtext-talk": "\"[[:$1]]\" og dets tilhørende side er blevet fjernet fra din [[Special:Watchlist|overvågningsliste]].",
        "removedwatchtext-short": "Siden \"$1\" er blevet fjernet fra din overvågningsliste.",
        "watch": "Overvåg",
        "watchthispage": "Overvåg side",
index a717dfa..bb81684 100644 (file)
        "ipb_expiry_old": "Der Zeitpunkt des Ablaufs liegt in der Vergangenheit.",
        "ipb_expiry_temp": "Benutzernamens-Sperren mit der Verstecken-Option müssen permanent sein.",
        "ipb_hide_invalid": "Dieses Konto kann nicht unterdrückt werden, da es mehr als {{PLURAL:$1|eine Bearbeitung|$1 Bearbeitungen}} aufweist.",
+       "ipb_hide_partial": "Versteckte Benutzernamenssperren müssen websiteweite Sperren sein.",
        "ipb_already_blocked": "„$1“ ist bereits gesperrt",
        "ipb-needreblock": "„$1“ ist bereits gesperrt. Möchtest du die Sperrparameter ändern?",
        "ipb-otherblocks-header": "Andere {{PLURAL:$1|Sperre|Sperren}}",
index ef219e1..da46154 100644 (file)
@@ -43,7 +43,7 @@
        "tog-extendwatchlist": "Lista seyrkerdışi hera bıke ke vurnayışi pêro basê, tenya tewr peyêni nê",
        "tog-usenewrc": "Vurnayışê ke pela vurnayışanê peyênan û lista seyrkerdışi derê inan grube ke.",
        "tog-numberheadings": "Sernuşteyan be xo numre cı şane",
-       "tog-editondblclick": "Per virnayisi di dilet klik bike (JavaScript lazımo)",
+       "tog-editondblclick": "Duble tıknayış de pelan bıvurnê",
        "tog-editsectiononrightclick": "Qısıman be tıknayışê serrêze ra ebe gocega raşte bıvurne (JavaScript lazımo)",
        "tog-watchcreations": "Pelê ke mı afernayê u dosyeyê ke mı bar kerdê lista mına seyrkerdışi ke",
        "tog-watchdefault": "Pel u dosyeyê ke mı vurnayê lista mına seyrkerdışi ke",
        "tog-minordefault": "Vurnayışanê xo pêrune ''vurnayışo qıckek'' nışan bıde",
        "tog-previewontop": "Verqayti pela nuştışi ser de bımocne",
        "tog-previewonfirst": "Vurnayışo verên de verqayti tım bımotne",
-       "tog-enotifwatchlistpages": "Jû pele ya ki dosyawa ke lista mına seyrkerdışi de vurnae, mı rê e-mail bırışe",
+       "tog-enotifwatchlistpages": "Lista mına seyr kerdışi de yew dosya ya zi pele vuriyenase, mı rê e-posta bırışe",
        "tog-enotifusertalkpages": "Pela mına werênayışi ke vurnayê mı rê e-poste bırışe",
        "tog-enotifminoredits": "Pelan de vurnayışanê qıckekan u dosyan de ki mı rê e-mail bırışe",
        "tog-enotifrevealaddr": "Adresa e-posteyê mı posteyê xeberan de bımocne",
        "tog-shownumberswatching": "Amarê karberanê seyr kerdoğan bımotne",
        "tog-oldsig": "İmzaya mewcud:",
-       "tog-fancysig": "İmza rê mameleyê wikimeqaley bıke (bê gıreyo otomatik)",
+       "tog-fancysig": "İmza goreyê metınê wikiyi (bê gıreyo otomatik) ra bıgurene",
        "tog-uselivepreview": "Bêbarkerdışê pele verqayti bımocne",
        "tog-forceeditsummary": "Mı ke xulasa veng verdaye, hay a mı ser de",
        "tog-watchlisthideown": "Vurnayışanê mı lista mına seyr kerdışi de bınımne",
        "broken-file-category": "Perri be linkanê dosya çewte",
        "categoryviewer-pagedlinks": "($1) ($2)",
        "about": "Heqa",
-       "article": "Wesiqe",
+       "article": "Pela zerreki",
        "newwindow": "(pençerey newey de beno a)",
        "cancel": "Bıtexelne",
        "moredotdotdot": "Vêşi...",
        "morenotlisted": "Na lista qay kemi ya.",
-       "mypage": "Per",
+       "mypage": "Pele",
        "mytalk": "Werênayış",
        "anontalk": "Werênayış",
        "navigation": "Pusula",
        "showtoc": "bımocne",
        "hidetoc": "bınımne",
        "collapsible-collapse": "Teng ke",
-       "collapsible-expand": "Hera ke",
+       "collapsible-expand": "Hera ke",
        "confirmable-confirm": "{{GENDER:$1|Şıma}} pêbawerê?",
        "confirmable-yes": "Eya",
        "confirmable-no": "Nê",
        "red-link-title": "$1 (pele çıniya)",
        "sort-descending": "Rêzkerdışo kêmbiyaye",
        "sort-ascending": "Rêzkerdışo zêdiyaye",
-       "nstab-main": "Perre",
+       "nstab-main": "Pele",
        "nstab-user": "Pera karberi",
        "nstab-media": "Perra medya",
        "nstab-special": "Perra xısusiye",
        "ns-specialprotected": "Pelê xısusiyi nêşenê bıvurriyê.",
        "titleprotected": "No sername terefê [[User:$1|$1]] ra, afernayene ra şevekiyayo.\nSebebê xo <em>$2</em> dero.",
        "filereadonlyerror": "Dosyay vurnayışê \"$1\" nê abêno lakin depoy dosya da \"$2\" mod dê  salt wendi de yo.\n\nXızmetkarê  kılit kerdışi wa bewniro enay wa çım ra ravyarn o: \"$3\".",
+       "invalidtitle": "Sernuşteyo nêravêrde",
        "invalidtitle-knownnamespace": "Canemey \"$2\" u metnê \"$3\" xırabo",
        "invalidtitle-unknownnamespace": "Sernameye nêşınasiya yana amraiya canameyo  $1 u metno \"$2\" xırab",
        "exception-nologin": "Şıma cıkewtış nêvıraşto",
        "userlogin-remembermypassword": "Mı biya xo viri",
        "userlogin-signwithsecure": "Ebe teqdimkerê asayişın cıkewe",
        "cannotlogin-title": "Cı nëkewtë",
+       "cannotlogin-text": "Cıkewtış mımkın niyo",
        "cannotloginnow-title": "Enewke ronıştışo nêabeno",
        "cannotloginnow-text": "$1 karkerdışa ronıştış akerdış mıkum niyo.",
        "cannotcreateaccount-title": "Nêşenay hesab rakerê",
        "headline_tip": "Sewiya 2ıne sername",
        "nowiki_sample": "metnê formatkerdey berze etıya",
        "nowiki_tip": "Goş formatê Wiki ra mekûwe",
-       "image_sample": "Misal resim.jpg",
+       "image_sample": "Nımune.jpg",
        "image_tip": "Dosya tewrkerdiye",
-       "media_sample": "misal.jpg",
+       "media_sample": "Nımune.ogg",
        "media_tip": "Gırey dosye",
        "sig_tip": "İmzay şıma be morê zemani",
        "hr_tip": "Xeta verardiye (teserrufın bıgureyne/bıxebetne)",
        "savechanges": "Vurnayışan qeyd ke",
        "publishpage": "Riperri bare ke",
        "publishchanges": "Vırnayışan qeyd ke",
+       "savearticle-start": "Pele qeyd ke...",
+       "savechanges-start": "Vurnayışan qeyd ke...",
+       "publishpage-start": "Pele weşane...",
+       "publishchanges-start": "Vurnayışan weşane...",
        "preview": "Verqayt",
        "showpreview": "Verasayışi bımocne",
        "showdiff": "Vurnayışan bımocne",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) bıviné",
        "searchmenu-exists": "''Ena 'Wikipediya de ser \"[[:$1]]\" yew pel esto'''",
        "searchmenu-new": "<strong>Ena wiki de perra \"[[:$1]]\" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi peyniyanê cıgeyrayışê xo bıvênê.}}",
-       "searchprofile-articles": "Perrê zerreki",
+       "searchprofile-articles": "Pelê zerreki",
        "searchprofile-images": "Multimedya",
        "searchprofile-everything": "Pêro çi",
        "searchprofile-advanced": "Herayen",
        "searchprofile-everything-tooltip": "Tedeesteyan hemine cı geyre (pelanê werênayışi zi tey)",
        "searchprofile-advanced-tooltip": "Cayê namanê xısusiyan de cı geyre",
        "search-result-size": "$1 ({{PLURAL:$2|1 çeku|$2 çekuy}})",
-       "search-result-category-size": "{{PLURAL:$1|1 eza|$1 ezayan}} ({{PLURAL:$2|1 kategoriyê bini|$2 kategirayanê binan}}, {{PLURAL:$3|1 dosya|$3 dosyayan}})",
+       "search-result-category-size": "{{PLURAL:$1|1 eza|$1 ezayi}} ({{PLURAL:$2|1 kategoriya bınêne|$2 kategoriyê bınêni}}, {{PLURAL:$3|1 dosya|$3 dosyeyi}})",
        "search-redirect": "($1 ra kırışiyaya)",
        "search-section": "(qısmê $1)",
        "search-category": "(kategoriye $1)",
        "prefs-watchlist-managetokens": "Kılitan idare ke",
        "prefs-misc": "ê bini",
        "prefs-resetpass": "Parola bıvurne",
-       "prefs-changeemail": "E-postay bıvurne yana wedarne",
+       "prefs-changeemail": "E-postay bıvurne ya zi wedarne",
        "prefs-setemail": "E-posta adresiyê xo saz kerê",
        "prefs-email": "Tercihê e-maili",
        "prefs-rendering": "Asayış",
        "saveprefs": "Qeyd ke",
-       "restoreprefs": "Sazanê hesıbyaya pêron newe ke",
+       "restoreprefs": "Eyaranê sıfteyan pêrıne peyser bar ke (leteyanê pêroyınan de)",
        "prefs-editing": "Vırnayış",
        "searchresultshead": "Cı geyre",
        "stub-threshold": "Qandé Taslağ  formati  sinor:$1",
        "recentchangesdays": "Rocê ke vurnayışanê peyênan de bıasê:",
        "recentchangesdays-max": "Tewr zaf $1 {{PLURAL:$1|roc|roci}}",
        "recentchangescount": "Halê est-amardışi ra mocnayışi rê amarê vırnayışan:",
-       "prefs-help-recentchangescount": "Ney de vurnayışê peyêni, tarixê pelan u cıkewteni asenê.",
+       "prefs-help-recentchangescount": "Reqemo azami: 1000",
        "prefs-help-watchlist-token2": "Na pawıtış nımnayi kılta listada şımaya.\nOke kıliti zano şeno listeya tamaşann bıvino. Poğta coy ra kesiya me hesırne. \n[[Special:ResetTokens|Na kıliti reset kerdışi re tiyay bıploğne]].",
        "savedprefs": "Tecihê şıma qeyd biy.",
        "savedrights": "{{GENDER:$1|$1}}  gruba karberi qeyd  biya.",
        "timezoneregion-indian": "Okyanuso Hind",
        "timezoneregion-pacific": "Okyanuso Pasifik",
        "allowemail": "Karberê bini wa bışê mı rê e-posta bırışê.",
-       "email-allow-new-users-label": "Karberanê neweyan ra epostegırotışi rê mısade ke",
+       "email-allow-new-users-label": "Karberanê neweyan ra eposte gırotışi rê mısade bıdê",
        "email-blacklist-label": "Wa nê karberi mı rê mesac nêrışê:",
        "prefs-searchoptions": "Cı geyre",
        "prefs-namespaces": "Heruna naman",
        "prefs-common-config": "CSS/JSON/JavaScripto ke seba heme cildan rê vıla biyo:",
        "prefs-reset-intro": "ena pele de şıma tercihanê xo şenê bıçarnê be tercihanê keyepelê ke verê coy eyar biy.\nNa game tepeya nêerziyena.",
        "prefs-emailconfirm-label": "Tesdiqiya E-posta:",
-       "youremail": "E-Mail (mecbur niyo) *:",
+       "youremail": "E-posta:",
        "username": "{{GENDER:$1|Namey karberi}}:",
        "prefs-memberingroups": "{{GENDER:$2|Ezayê}} {{PLURAL:$1|grube|gruban}}:",
        "prefs-memberingroups-type": "$1",
        "yourvariant": "Varyante miyandê zuwani:",
        "prefs-help-variant": "Zerrey ena viki mocnayışi rê varyant yana ortografi re şıre tercihan dê xo.",
        "yournick": "Leqebe:",
-       "prefs-help-signature": "Peran de vatenana de vatışi\"<nowiki>~~~~</nowiki>\" ya do imza bé, no bahdo beno çerğé imza u wahdey zemani",
+       "prefs-help-signature": "Mışewreyê ke pelanê werênayışi derê, gani be \"<nowiki>~~~~</nowiki>\" ra imza bıbê, no bahdo beno çerxê imza û wadeyê zemani.",
        "badsig": "Îmzayê tu raşt niyo.\nEtiketê HTMLî kontrol bike.",
        "badsiglength": "İmzaya şıma zaf derga.\nA gani be $1 {{PLURAL:$1|karakter|karakteran}} ra zêde mebo.",
        "yourgender": "Şeklê xitabi?",
        "prefs-help-email-others": "Pera ğoya kerderi de zew link vırazése karberé bini şımaré şenê mesac bırşé. Lakin e-posta adresa şıma héç cayé de niasena.",
        "prefs-help-email-required": "E-mail adrese mecburiya.",
        "prefs-info": "Melumato bıngehên",
-       "prefs-i18n": "Şar şélıg kerdış",
+       "prefs-i18n": "Beynelmillel kerdış",
        "prefs-signature": "İmza",
        "prefs-dateformat": "Formatê tarixi",
-       "prefs-timeoffset": "Wext offset",
+       "prefs-timeoffset": "Ferqê seate",
        "prefs-advancedediting": "Herayen weçinayış",
+       "prefs-developertools": "Hacetê raverberdoği",
        "prefs-editor": "Vurnayoğ",
        "prefs-preview": "Verqayt",
        "prefs-advancedrc": "Tercihê raverberdey",
        "group-autoconfirmed": "Karberê ke otomatikmen biyê araşt",
        "group-bot": "Roboti",
        "group-sysop": "İdarekari",
+       "group-interface-admin": "İdarekarê namnişani",
        "group-bureaucrat": "Buroqrati",
        "group-suppress": "Pawıteri",
        "group-all": "(pêro)",
        "group-autoconfirmed-member": "{{GENDER:$1|Karberê ke otomatikmen biyê araşt}}",
        "group-bot-member": "{{GENDER:$1|bot}}",
        "group-sysop-member": "{{GENDER:$1|İdarekar}}",
+       "group-interface-admin-member": "{{GENDER:$1|idarekarê namnişani}}",
        "group-bureaucrat-member": "{{GENDER:$1|buroqrat}}",
        "group-suppress-member": "{{GENDER:$1|Temaşekar}}",
        "grouppage-user": "{{ns:project}}:Karberi",
        "grouppage-autoconfirmed": "{{ns:project}}:Karberê ke otomatikmen biyê araşt",
        "grouppage-bot": "{{ns:project}}:Boti",
        "grouppage-sysop": "{{ns:project}}:İdarekeri",
+       "grouppage-interface-admin": "{{ns:project}}:İdarekarê namnişani",
        "grouppage-bureaucrat": "{{ns:project}}:Burokrati",
        "grouppage-suppress": "{{ns:project}}:Teftişkar",
        "right-read": "Pera bıwané",
        "right-editusercss": "Dosyanê CSSiê karberanê binan sero bıgureye",
        "right-edituserjson": "Dosyanê JSONiyê karberanê binan bıvurne",
        "right-edituserjs": "Dosyanê JSiê karberanê binan sero bıgureye",
+       "right-editsitecss": "Sitewide CSS bıvurne",
+       "right-editsitejson": "Sitewide JSON bıvurne",
+       "right-editsitejs": "Sitewide JavaScripti bıvurne",
        "right-editmyusercss": "CSS dosyaya karberinda ğo timar ke",
        "right-editmyuserjson": "Dosyanê JSONiyê xo bıvurne",
        "right-editmyuserjs": "JavaScript dosyaya karberinda ğo timar ke",
        "right-sendemail": "Karberanê binî ra e-mail bişirav",
        "right-managechangetags": "[[Special:Tags|Etiketi]] vıraz u aktiv (me)ke",
        "right-applychangetags": "[[Special:Tags|Etiketa]]  vurnayışana piya dezge fi.",
+       "grant-generic": "\"$1\" paketa heqan",
        "grant-group-page-interaction": "Peran na tesiri",
        "grant-group-file-interaction": "Medya na tesiri",
        "grant-group-watchlist-interaction": "Lista da xoya tesir",
        "grant-createaccount": "Hesab vıraze",
        "grant-createeditmovepage": "Perer vırazê, bıvurnê u berê",
        "grant-delete": "Besternayış, revizyon  u qeydé peran",
-       "grant-editinterface": "Canameyê MediaWiki u CSS/javScripta karberi bıvurnê",
-       "grant-editmycssjs": "CSS/JavaScripta karberiya xo bıvurnê",
+       "grant-editinterface": "Canameyê MediaWiki û sitewide/JSONê karberi bıvurnê",
+       "grant-editmycssjs": "CSS/JSON/JavaScriptê karberiya xo bıvurnê",
        "grant-editmyoptions": "Tercihanê xo û awankerdışê JSONi bıvurne",
        "grant-editmywatchlist": "Listeyseyran de xo bıvırne",
+       "grant-editsiteconfig": "Sitewide û CSS/JSê karberi bıvurne",
        "grant-editpage": "Pela mewcude bıvurne",
        "grant-editprotected": "Pela mewcude bıvurne",
        "grant-highvolume": "Vengê berzi dayış",
        "grant-oversight": "Karberan u ploğyayê revizyona bınımn",
-       "grant-patrol": "{{GENDER:$1|$1i çım ra viyarna ra:$1}}",
+       "grant-patrol": "Çım berzê vurnayışanê pele",
+       "grant-privateinfo": "Bıresê melumatê xısusiyi",
        "grant-protect": "Şeveknayış u wedarıtışê şeveknayışê pelan",
        "grant-rollback": "Pelanê peysergırewtışi bıvurne",
        "grant-sendemail": "Karberanê binan rê e-posta bırışê",
        "action-deleterevision": "revizyoni besternê",
        "action-deletelogentry": "qeydanê cıkewtışan bestere",
        "action-deletedhistory": "verora esteriya perrer bıvin",
+       "action-deletedtext": "revizyonê metıniyê esterıteyi bımocne",
        "action-browsearchive": "pelanê esterıteyan bıgeyre",
        "action-undelete": "Ena perre mesterê",
        "action-suppressrevision": "revizyonê nımnayi bıvin u timar kı.",
        "action-editcontentmodel": "Zerrekê modela yu perer timar ke",
        "action-managechangetags": "Vıraz u etiketa aktiv (me) ke",
        "action-applychangetags": "Vurnayışana piya etiket kerdışi zi dezge fi",
+       "action-deletechangetags": "etitikan danegeh ra bestere",
        "action-purge": "Ane perer newe ke",
        "nchanges": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|ziyaretê peyêni ra nata}}",
        "recentchanges-legend-plusminus": "''(±123)''",
        "recentchanges-submit": "Bımocne",
        "rcfilters-tag-remove": "'$1' wedare",
+       "rcfilters-legend-heading": "<strong>Lista kılmkerdışan:</strong>",
+       "rcfilters-other-review-tools": "Hacetê çımeştışê bini",
+       "rcfilters-activefilters": "Parzûnê aktifi",
        "rcfilters-activefilters-hide": "Bınımne",
        "rcfilters-activefilters-show": "Bımocne",
        "rcfilters-activefilters-hide-tooltip": "Heruna parzûnanê aktifan bınımne",
        "rcfilters-activefilters-show-tooltip": "Heruna parzûnanê aktifan bımocne",
+       "rcfilters-advancedfilters": "Parzûnê raverşiyayeyi",
        "rcfilters-limit-title": "Neticeyê ke bımocniyê",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}, $2",
        "rcfilters-days-title": "Rocê peyêni",
        "rcfilters-hours-title": "Seatê peyêni",
        "rcfilters-days-show-days": "($1 {{PLURAL:$1|roce|roci}})",
        "rcfilters-days-show-hours": "($1 {{PLURAL:$1|saete|saeti}})",
+       "rcfilters-quickfilters": "Parzûnê qeydbiyayeyi",
+       "rcfilters-quickfilters-placeholder-title": "Qet yew parzûn qeyd nêbiyo",
+       "rcfilters-savedqueries-defaultlabel": "Parzûnê qeydbiyayeyi",
        "rcfilters-savedqueries-rename": "Reyna name ke",
        "rcfilters-savedqueries-setdefault": "Wa hesabiyaye bımano",
        "rcfilters-savedqueries-remove": "Bestere",
        "rcfilters-savedqueries-apply-label": "Parzûn vıraze",
        "rcfilters-savedqueries-apply-and-setdefault-label": "Parzûno hesebiyaye vıraze",
        "rcfilters-savedqueries-cancel-label": "Bıtexelne",
+       "rcfilters-clear-all-filters": "Parzûnan pêro pak kerê",
+       "rcfilters-show-new-changes": "Vurnayışanê neweyan bımocne",
+       "rcfilters-search-placeholder": "Vurnayışanê peyênan parzûn kerê (menuyi bıgurenê ya zi nameyê parzûni cıgeyrê)",
+       "rcfilters-invalid-filter": "Parzûno nêravêrde",
+       "rcfilters-empty-filter": "Parzûnê aktifi çıniyê. İştırakê cı pêro mocniyenê.",
        "rcfilters-filterlist-title": "Parzûni",
        "rcfilters-filterlist-whatsthis": "Nê çıtewri guriyenê?",
+       "rcfilters-highlightmenu-title": "Yew reng weçine",
+       "rcfilters-filterlist-noresults": "Parzûni nêvêniyayi",
        "rcfilters-filtergroup-authorship": "Wayiriya iştırakan",
        "rcfilters-filter-editsbyself-label": "Vurnayışê şıma",
        "rcfilters-filter-editsbyself-description": "İştırakê şıma.",
        "rcfilters-filter-editsbyother-label": "Ê binan ra vurnayışi",
        "rcfilters-filter-user-experience-level-registered-label": "Qeydıni",
+       "rcfilters-filter-user-experience-level-registered-description": "İdarekarê cıkewteyi.",
+       "rcfilters-filter-user-experience-level-unregistered-label": "Bêqeydıni",
        "rcfilters-filter-user-experience-level-newcomer-label": "Ameyayeyê neweyi",
        "rcfilters-filter-user-experience-level-newcomer-description": "Karberê qeydınê ke 10 ra kemi vurnayışi ya zi 4 rocan ra fealiyetê xo estê.",
        "rcfilters-filter-user-experience-level-learner-label": "Musayoği",
+       "rcfilters-filter-user-experience-level-experienced-label": "Karberê mısayeyi",
        "rcfilters-filter-bots-label": "Bot",
        "rcfilters-filter-humans-label": "İnsan (bot niyo)",
        "rcfilters-filter-humans-description": "Terefê insanan ra vuriyayışi.",
+       "rcfilters-filter-reviewstatus-unpatrolled-label": "Desturê dewriya ra nêvêrdo",
        "rcfilters-filter-reviewstatus-auto-label": "Otomatik kontrol bi",
        "rcfilters-filtergroup-significance": "Gıraniye",
+       "rcfilters-filter-minor-label": "Vurriyayışê werdiyi",
+       "rcfilters-filter-minor-description": "Vurriyayışê ke nuştekari vurnayışo werdi etiket kerdê.",
+       "rcfilters-filter-major-label": "Vurriyayışê ke werdi niyê",
        "rcfilters-filtergroup-watchlist": "Pelê ke seyr benê",
        "rcfilters-filter-watchlist-watched-label": "Lista seyrkerdışi de",
+       "rcfilters-filter-watchlist-watchednew-label": "Vurnayışê lista seykerdışiya newiye",
        "rcfilters-filter-watchlist-notwatched-label": "Lista seyrkerdışi de niya",
        "rcfilters-filtergroup-watchlistactivity": "Fealiyetê pela seyrkerdışi",
        "rcfilters-filter-watchlistactivity-unseen-label": "Vuriyayışê ke nêvêniyê",
        "rcfilters-filtergroup-changetype": "Tewrê vurnayışi",
        "rcfilters-filter-pageedits-label": "Vuriyayışê pelan",
        "rcfilters-filter-newpages-label": "Vıraştışê pelan",
+       "rcfilters-filter-newpages-description": "Vurnayışê ke pelanê newiyab vırazenê.",
        "rcfilters-filter-categorization-label": "Vuriyayışê kategoriyan",
        "rcfilters-filter-categorization-description": "Kategoriyan ra qeydê cıkerdış u wedardışê pelan.",
        "rcfilters-filtergroup-lastRevision": "Çımraviyarnayışê tewr peyêni",
        "rcfilters-filter-previousrevision-label": "Çımraviyarnayışo peyên niyo",
        "rcfilters-filter-excluded": "Xarıc",
        "rcfilters-tag-prefix-namespace-inverted": "$1 <strong>:nê</strong>",
+       "rcfilters-exclude-button-off": "Weçinayeyi ciya bıtepışê",
+       "rcfilters-exclude-button-on": "Weçinayeyo ciya",
+       "rcfilters-view-tags": "Vurnayışê etiketıni",
+       "rcfilters-view-return-to-default-tooltip": "Peyser şo parzûnê menuyê bıngehi",
+       "rcfilters-liveupdates-button": "Rocaneyê ganıni",
+       "rcfilters-liveupdates-button-title-on": "Rocaneyanê cındeyan ragê",
+       "rcfilters-preference-label": "Mabeynrıyê non-JavaScript'i bıkarne",
+       "rcfilters-watchlist-preference-label": "Mabeynrıyê non-JavaScript'i bıkarne",
+       "rcfilters-target-page-placeholder": "Yew nameyê pele (ya zi kategoriye) cı kerê",
        "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>",
+       "rclistfromreset": "Weçinayışê tarixi ragoze",
        "rclistfrom": "$3 sehat $2 ra tepiya vurnayışanê neweyan bımotne",
        "rcshowhideminor": "Vırnayışê werdiy $1",
        "rcshowhideminor-show": "Bımocne",
        "upload-too-many-redirects": "Eno URL de zaf redireksiyonî esto.",
        "upload-http-error": "Yew ğeletê HTTPî biyo: $1",
        "upload-copy-upload-invalid-domain": "Na domain ra kopyayê barkerdışanê nêbenê.",
-       "upload-dialog-title": "Dosya bar kı",
+       "upload-dialog-title": "Dosya bar ke",
        "upload-dialog-button-cancel": "Bıtexelne",
        "upload-dialog-button-back": "Peyser",
        "upload-dialog-button-done": "Temam",
-       "upload-dialog-button-save": "Bışevekne",
+       "upload-dialog-button-save": "Qeyd ke",
        "upload-dialog-button-upload": "Bar ke",
        "upload-form-label-infoform-title": "Teferuati",
        "upload-form-label-infoform-name": "Name",
        "uploadstash-errclear": "Besternayışê dosyayan nêbı",
        "uploadstash-refresh": "Listanê dosyayan aneweke",
        "uploadstash-thumbnail": "asayışê qıckeki bıvêne",
+       "uploadstash-bad-path": "Raye mewcude niya",
+       "uploadstash-bad-path-invalid": "Raye ravêrdiye niya.",
+       "uploadstash-bad-path-unknown-type": "Tewro ke nêşınasiyeno \"$1\".",
+       "uploadstash-file-not-found-no-thumb": "Resımo werdi nêgêriya.",
+       "uploadstash-no-extension": "Derganiya xırabıne.",
+       "uploadstash-zero-length": "Ebadê dosya sıfıro.",
        "invalid-chunk-offset": "Ofseto nêravyarde",
        "img-auth-accessdenied": "Cıresnayış vındarnayo.",
        "img-auth-nopathinfo": "PATH_INFO kemiyo.\nTeqdimkerê şıma seba ravurnayışê nê melumati eyar nêkerdo.\nBeno ke be CGI-bıngeyın bo u img_auth rê desteg nêbeno.\nhttps://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization Selahiyetê resımi bıvêne.",
        "http-timed-out": "Waştişê HTTP qediya.",
        "http-curl-error": "Xetayê URLi: $1",
        "http-bad-status": "Waştişê tu HTTP yew problem biya: $1 $2",
+       "http-internal-error": "Xetaya daxiliye HTTP.",
        "upload-curl-error6": "URL rê nieşkeno biraso",
        "upload-curl-error6-text": "URL yo ke nişane biyo nêresiyeno\nkerem kerê bıewnê URLyê şıma raşta ya zi bıewnê keyepel akerdeyo.",
        "upload-curl-error28": "Wextê bar kerdişî qediya",
        "upload-curl-error28-text": "cewab dayişê no keyepel zaf hereyo.\nbıewnê keyepel akerdeyo ya zi bıne vınderê u newe ra tesel bıkerê.\nkeyepel nıka zaf meşğulo yew dema herayi de newe ra tesel bıkerê.",
-       "license": "Lisans:",
+       "license": "Lisansdayış:",
        "license-header": "Lisansdayış",
        "nolicense": "Theba nêweçineya",
        "licenses-edit": "Weçenega lisansi bıvurnê",
        "listfiles_size": "Gırdiye",
        "listfiles_description": "Şınasnayış",
        "listfiles_count": "Versiyoni",
-       "listfiles-show-all": "Asayışa versiyonandé verénan",
+       "listfiles-show-all": "Versiyonanê dosyayê verênan zerre ke",
        "listfiles-latestversion": "Versiyono verin",
        "listfiles-latestversion-yes": "Eya",
        "listfiles-latestversion-no": "Nê",
        "statistics-header-edits": "İstatistikê vırnayışan",
        "statistics-header-users": "İstatistikê karberi",
        "statistics-header-hooks": "Yewbina istatistiki",
-       "statistics-articles": "Perê zerreki",
-       "statistics-pages": "Peri",
-       "statistics-pages-desc": "Peri heme, kategoriy, şabloni, moduli uçb.",
-       "statistics-files": "Barneyaye dosyey",
+       "statistics-articles": "Pelê zerreki",
+       "statistics-pages": "Peli",
+       "statistics-pages-desc": "Pelê wikiyi pêro, pelê werênayışi, serşıkıtışi uçb.",
+       "statistics-files": "Dosyeyê ke bar biyê",
        "statistics-edits": "Ronayen da {{SITENAME}} ra newke amora vıryayışan",
        "statistics-edits-average": "Her pele sero nısbi vurnayış",
        "statistics-users": "Karberê qeydıni",
        "statistics-users-active": "Karberê aktifi",
-       "statistics-users-active-desc": "{{PLURAL:$1|roco peyin de|$1 roco peyin de}} karber ê ke kar kerdê.",
+       "statistics-users-active-desc": "Karberê ke {{PLURAL:$1|roca peyêne de|$1 rocanê peyênan de}} iştırak kerdo.",
        "pageswithprop": "Peli be yew xısusiyetê pele",
        "pageswithprop-legend": "Peli be yew xısusiyetê pele",
        "pageswithprop-text": "Na per pimanen pera kena liste.",
        "specialpage-empty": "Seba na rapore netice çıniyo.",
        "lonelypages": "Pelê seyi",
        "lonelypagestext": "Ena pelî link nibiyê ya zi pelanê binî {{SITENAME}} de transclude biy.",
-       "uncategorizedpages": "Pelê ke kategorize nêbiyê",
-       "uncategorizedcategories": "Kategoriyê ke kategorize nêbiyê",
-       "uncategorizedimages": "Dosyeyê kategorinêbiyay.",
+       "uncategorizedpages": "Pelê ke bêkategoriyê",
+       "uncategorizedcategories": "Kategoriyê ke bêkategoriyê",
+       "uncategorizedimages": "Dosyeyê ke bêkategoriyê",
        "uncategorizedtemplates": "Şablonê ke bêkategoriyê",
        "unusedcategories": "Kategoriyê ke nêgureniyê",
        "unusedimages": "Dosyeyê ke nêguriyenê",
        "wantedtemplates": "Şablonê ke waziyenê",
        "mostlinked": "Pelî ke tewr zafî lînk bîy.",
        "mostlinkedcategories": "Kategoriyê ke tewr zehf meqaley tede estê",
-       "mostlinkedtemplates": "Perrê ke vêşêr gırweniyenê",
+       "mostlinkedtemplates": "Pelê ke vêşêri guriyenê",
        "mostcategories": "Pelan ke tewr zaf kategorî estê.",
        "mostimages": "Dosyayan ke tewr zaf link estê.",
        "mostinterwikis": "Pelan ke tewr zaf interwiki biyê.",
        "prefixindex": "Pêro peli be verbend",
        "prefixindex-namespace": "Peleyê Veroleyıni ($1 cay nami)",
        "prefixindex-submit": "Bımocne",
-       "prefixindex-strip": "Listeya réz bıyayışi",
+       "prefixindex-strip": "Neticeyan de prefiksi bınımne",
        "shortpages": "Pelê kılmi",
        "longpages": "Pelê dergi",
-       "deadendpages": "Pelê nêgıredayeyi",
+       "deadendpages": "Pelê ke pelanê binan rê gıreyê cı çıniyo",
        "deadendpagestext": "Ena pelan ke {{SITENAME}} de zerrî ey de link çini yo.",
-       "protectedpages": "Pellê kı pawıyayeyè",
+       "protectedpages": "Pelê pawıteyi",
        "protectedpages-filters": "Parzûni:",
        "protectedpages-indef": "têna pawıteyê bêmuddeti",
        "protectedpages-summary": "Listeya ena peler newke pawıtiya.Sername de  ena lista rê pawıte vıraştışi rê [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]] bıvinê.",
        "protectedtitlesempty": "pê ney parametreyan sernuşteyê pawite çinê",
        "protectedtitles-submit": "Sereki bımotne",
        "listusers": "Lista karberan",
-       "listusers-editsonly": "Teyna karberanê vırnayış kerdoğan bımotne",
-       "listusers-creationsort": "goreyê wextê vıraştışi rêz ker",
-       "listusers-desc": "Kemeyen rézed ratn",
+       "listusers-editsonly": "Tenya karberanê vurnayışkerdoğan bımocne",
+       "listusers-temporarygroupsonly": "Tenya karberanê ke grubanê vêrdeyan derê, inan bımocne",
+       "listusers-creationsort": "Goreyê wextê vıraştışi rêz ke",
+       "listusers-desc": "Rêza kemibiyayışi de rêz ke",
        "usereditcount": "$1 {{PLURAL:$1|vurnayîş|vurnayîşî}}",
        "usercreated": "$2 de $1 {{GENDER:$3|viraziya}}",
        "newpages": "Perrê newey",
        "apisandbox-submit": "Bıwazê",
        "apisandbox-reset": "Bestere",
        "apisandbox-retry": "Anciya bıcerrebne",
-       "apisandbox-examples": "Misali",
+       "apisandbox-helpurls": "Linkê peşti",
+       "apisandbox-examples": "Nımuneyi",
        "apisandbox-dynamic-parameters": "Parametreya debyayi",
        "apisandbox-dynamic-parameters-add-label": "Parametre dek:",
        "apisandbox-dynamic-parameters-add-placeholder": "Nmaey parametrey",
+       "apisandbox-dynamic-error-exists": "Yew parametre be nameyê '%1' ra xora esto.",
+       "apisandbox-deprecated-parameters": "Parametreyo ke qebul nêbiyo",
+       "apisandbox-fetch-token": "Cayê otomatiki pırr ke",
+       "apisandbox-add-multi": "Cı ke",
        "apisandbox-submit-invalid-fields-title": "Tay çiy ters şı",
        "apisandbox-results": "Peyniy",
        "apisandbox-sending-request": "API waştış rışêno...",
        "apisandbox-request-url-label": "URL waştış:",
+       "apisandbox-request-json-label": "Waştışê JSON'i:",
        "apisandbox-request-time": "Demê waştışi: {{PLURAL:$1|$1 ms}}",
        "apisandbox-continue": "Dewam ke",
+       "apisandbox-continue-clear": "Pak ke",
+       "apisandbox-multivalue-all-namespaces": "$1 (heruna nameyan pêro)",
+       "apisandbox-multivalue-all-values": "$1 (Erci pêro)",
        "booksources": "Çımey kıtabi",
        "booksources-search-legend": "Seba çımeyanê kıtaban cı geyre",
        "booksources-isbn": "ISBN:",
        "speciallogtitlelabel": "Meqsed (sername ya zi {{ns:user}}:karberi rê nameyê karberi):",
        "log": "Qeydi",
        "logeventslist-submit": "Bımocne",
+       "logeventslist-patrol-log": "Rocekê dewriya",
+       "logeventslist-tag-log": "Rocekê etiketan",
        "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.",
        "checkbox-select": "Weçinaye: $1",
        "checkbox-all": "Pêro",
        "checkbox-none": "Temam",
-       "checkbox-invert": "Verdindayış",
+       "checkbox-invert": "Dimlaşt ke",
        "allpages": "Pêro peli",
        "nextpage": "Pela peyco ($1)",
        "prevpage": "Pela veri ($1)",
        "linksearch-text": "Jokeri ê zey \"*.wikipedia.org\"i benê ke bıgureniyê.\nTewr senık yew sewiya serêna cayê tesiri lazıma, mesela \"*.org\".<br />\nQeydeyê {{PLURAL:$2|protoqol|protoqoli}}:destegbiyayey: $1 (qet yew qeydeyo hesabiyaye http:// ke name nêbiyo).",
        "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:",
+       "listusersfrom": "Karberê ke tiya de destpêkenê, bımocne:",
        "listusers-submit": "Bımocne",
        "listusers-noresult": "karber nêdiyayo/a.",
        "listusers-blocked": "(blok biy)",
        "listgrouprights-group": "Grube",
        "listgrouprights-rights": "Heqqî",
        "listgrouprights-helppage": "Help:Heqqanê gruban",
-       "listgrouprights-members": "[listey ezayan]",
+       "listgrouprights-members": "(lista ezayan)",
        "listgrouprights-right-display": "<span class=\"listgrouprights-granted\">$1 <code>($2)</code></span>",
        "listgrouprights-right-revoked": "<span class=\"listgrouprights-revoked\">$1 <code>($2)</code></span>",
        "listgrouprights-addgroup": "{{PLURAL:$2|Grube|Gruban}} cı kerê: $1",
        "watchthispage": "Na pele de seyr ke",
        "unwatch": "Teqib meke",
        "unwatchthispage": "temaşa kerdışê peli vındarn.",
-       "notanarticle": "mebhesê peli niyo",
+       "notanarticle": "Yew pela zerreki niya",
        "notvisiblerev": "Revizyon esteriyayo",
        "watchlist-details": "Lista şımaya seyrkerdışi de (be pelanê werênayışi ra piya) {{PLURAL:$1|$1 pele esta|$1 peli estê}}.",
        "wlheader-enotif": "E-mail xeber dayiş abiyo.",
        "enotif_body_intro_moved": "{{SITENAME}} de pera $1 $PAGEEDITDATE de {{gender:$2|$2}}i berd, rewizyonê $3 bıvin.",
        "enotif_body_intro_restored": "{{SITENAME}} de pera $1 $PAGEEDITDATE de {{gender:$2|$2}}i timar ke, rewizyonê $3 bıvin.",
        "enotif_body_intro_changed": "{{SITENAME}} de pera $1 $PAGEEDITDATE de {{gender:$2|$2}}i vurne, rewizyonê $3 bıvin.",
-       "enotif_lastvisited": "ziyareta şıma ye peyini ra nata heme vuryayiş ê ke biyê bıewnê $1i re..",
-       "enotif_lastdiff": "qey vinayişê ney vurnayişi bıewnê pelê $1i",
+       "enotif_lastvisited": "Ziyaretê şımayê peyêni ra nata vurnayışi pêro, $1 bıvênê",
+       "enotif_lastdiff": "Nê vurnayışi bıvêne, $1 bewne",
        "enotif_anon_editor": "karbero anonim $1",
        "enotif_body": "Erciyayê $WATCHINGUSERNAME,\n\n$PAGEINTRO $NEWPAGE\n\neniya timaroği: $PAGESUMMARY $PAGEMINOREDIT\n\nTimaroğiya irtibat:\nmail: $PAGEEDITOR_EMAIL\nwiki: $PAGEEDITOR_WIKI\n\nno pel o ke behs beno heta ziyaret kerdışê yewna heli, mesajê vuriyayişi nêşawiyeno.\n\n           {{SITENAME}} sistemê hişyariyê keyepeli.\n\n--\nQey vurnayişê eyari:\n{{canonicalurl:{{#Special:Watchlist/edit}}}}\n\nQey vurnayişê eyaran de lista seyri:\n{{canonicalurl:{{#special:EditWatchlist}}}}\n\nQey wedarayişê ena pele liste xo ra seyr kerdişi, şo\n$UNWATCHURL\n\nQey hemkari u pêşniyazi:\n$HELPPAGE",
+       "enotif_minoredit": "No yew vırnayışo werdiyo",
        "created": "viraziya",
        "changed": "vurneya",
        "deletepage": "Pele bestere",
        "dellogpage": "Qeydê esterıtışi",
        "dellogpagetext": "listeya cêrıni heme qaydê hewn a kerdeyan o.",
        "deletionlog": "qeydê esterıtışi",
+       "log-name-create": "Rocekê vıraştena pelan",
+       "logentry-create-create": "$1, nameyê $3 ra yew pele {{GENDER:$2|vıraşte}}",
        "reverted": "revizyono verin tepiya anciyayo",
        "deletecomment": "Sebeb:",
        "deleteotherreason": "Sebebo bin:",
        "sessionfailure-title": "Seans xeripiya",
        "sessionfailure": "cıkewtışê hesabê şıma de yew problem aseno;\nno kar semedê dızdiyê hesabi ibtal biyo.\nkerem kerê \"tepiya\" şiyerê u pel o ke şıma tera ameyî u o pel newe ra bar kerê , newe ra tesel/cereb kerê.",
        "changecontentmodel": "Modelê zerrekê pele bıvurne",
+       "changecontentmodel-legend": "Modelê zerreki bıvurne",
        "changecontentmodel-title-label": "Sernameyê pele",
        "changecontentmodel-model-label": "Modelê zerrekiyo newe",
        "changecontentmodel-reason-label": "Sebeb:",
        "changecontentmodel-submit": "Bıvırne",
+       "changecontentmodel-success-title": "Modelê zerreki vurriya",
        "log-name-contentmodel": "Qeydê vurnayışanê modelê zerreki",
        "logentry-contentmodel-change-revertlink": "peyser biya",
        "logentry-contentmodel-change-revert": "peyser biya",
        "modifiedarticleprotection": "Qe \"[[$1]]\", seviye kılit kerdişi vurnayi biyo",
        "unprotectedarticle": "Starkerdışê \"[[$1]]\" hewadeya",
        "movedarticleprotection": "eyarê pawıtışi no \"[[$2]]\" peli ra kırışiya no \"[[$1]]\" peli",
+       "protectedarticle-comment": "\"[[$1]]\" {{GENDER:$2|sıtariya}}",
+       "modifiedarticleprotection-comment": "Seba \"[[$1]]\" rê {{GENDER:$2|sewiyaya şeveknayışi vurriye}}",
        "protect-title": "qey \"$1\" yew seviyaya pawıtışi bıvıcinê",
        "protect-title-notallowed": "Star kerdış sewiyeyê \"$1\" bıvinê",
        "prot_1movedto2": "nameyê [[$1]] peli yo newe: [[$2]]",
        "ipb-disableusertalk": "Pela xoya werênayışi bıvurne",
        "ipb-change-block": "Karberi be enê eyaran reyna piya bloke ke.",
        "ipb-confirm": "Bloke kerdışi tesdik ke",
+       "ipb-sitewide": "Site hemi de",
+       "ipb-partial": "Qısmi",
+       "ipb-pages-label": "Peli",
        "ipb-namespaces-label": "Heruna nameyan",
        "badipaddress": "Adresê IPî raşt niyo",
        "blockipsuccesssub": "Blok biyo",
        "unblocked-range": "Blokey $1'i wederya",
        "unblocked-id": "Blokê $1î wedariyayo",
        "blocklist": "Karberê kılitbiyayey",
+       "autoblocklist": "Blokeyê otomatiki",
        "autoblocklist-submit": "Cı geyre",
+       "autoblocklist-legend": "Lista blokanê otomatikan",
+       "autoblocklist-localblocks": "{{PLURAL:$1|otoblokoyo lokal|otoblokeyê lokali}}",
        "ipblocklist": "Karberê kılitbiyayey",
        "ipblocklist-legend": "Karberê kılit biyayey bıvin",
        "blocklist-userblocks": "Kılitkerdışê hesaban bınımne",
        "createaccountblock": "Hesab vıraştene men biya",
        "emailblock": "e-mail men kerd",
        "blocklist-nousertalk": "Perra xo'ya suhbeti vırnaene nêbena",
+       "blocklist-editing": "vurnayış",
+       "blocklist-editing-sitewide": "vurnayış (site hemi de)",
        "blocklist-editing-page": "peli",
        "blocklist-editing-ns": "Heruna nameyan",
        "ipblocklist-empty": "Listay kılit kerdışi venga.",
        "import-mapping-namespace": "Dek yu canamey miyan",
        "import-mapping-subpage": "Bınnpeley ena peler deyne azere ke",
        "import-upload-filename": "Nameyê dosyayi:",
+       "import-upload-username-prefix": "Prefiksê interwikiyi:",
        "import-comment": "Mışewre:",
        "importtext": "Kerem ke dosyay, çımeyê wiki ra pê [[Special:Export|kırıştışê teberdayişi]] bıdê teber, Komputerê xo de qeyd kerê u bar kerê tiya.",
        "importstart": "Pelan împort kenî",
        "tooltip-pt-login": "Mayê şıma ronıştış akerdışi rê dawet keme; labelê ronıştış mecburi niyo",
        "tooltip-pt-logout": "Bıveciye",
        "tooltip-pt-createaccount": "Şıma rê tewsiyey ma xorê jew hesab akerê. Fına zi hesab akerdış mecburi niyo.",
-       "tooltip-ca-talk": "Heqa zerreki de werênayış",
+       "tooltip-ca-talk": "Heqa pela zerreki de werênayış",
        "tooltip-ca-edit": "Ena pele bıvurne",
        "tooltip-ca-addsection": "Bınleteyo newe akerê",
        "tooltip-ca-viewsource": "Ena pele kılit biya.\nŞıma şenê çımeyê aye bıvênê",
        "pageinfo-display-title": "Sernuşteyo ke mosneyêno",
        "pageinfo-default-sort": "Hesıbyaye mırfeyo kılm",
        "pageinfo-length": "Derdeya pela (bayti heta)",
+       "pageinfo-namespace": "Heruna nameyi",
        "pageinfo-article-id": "Kamiya pele",
        "pageinfo-language": "Zıwanê zerreyê pele",
        "pageinfo-language-change": "bıvırne",
        "newimages-summary": "Ena pela xasi dosyayi ke peni de bar biyayeyi mocnane.",
        "newimages-legend": "Parzûn",
        "newimages-label": "Nameyê dosya ( ya zi parçe ey)",
+       "newimages-user": "Adresa IPyi ya zi nameyê karberi",
        "newimages-showbots": "Selaganë boti bıvin",
        "newimages-hidepatrolled": "Selaganë dewriyeyan bıvinë",
+       "newimages-mediatype": "Tewrê medya:",
        "noimages": "Çik çini yo.",
        "ilsubmit": "Cı geyre",
        "bydate": "Gorey zemani",
        "version-specialpages": "Perê hısusiy",
        "version-parserhooks": "Çengelê Parserî",
        "version-variables": "Vurnayeyî",
+       "version-editors": "Editori",
        "version-antispam": "Spam vındarnayış",
        "version-api": "API",
        "version-other": "Bin",
        "specialpages-group-changes": "Vırnayışê peyêni u qeydi",
        "specialpages-group-media": "Raporê medya û barkerdışi",
        "specialpages-group-users": "Karberi u heqê inan",
-       "specialpages-group-highuse": "Peleyê ke vêşi karênê",
+       "specialpages-group-highuse": "Pelê ke zêdêr gureniyenê",
        "specialpages-group-pages": "Listên pelan",
        "specialpages-group-pagetools": "Haletê pelan",
        "specialpages-group-wiki": "Melumat u haceti",
        "tag-filter": "Parzûnê [[Special:Tags|etiketi]]:",
        "tag-filter-submit": "Parzûn",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Etiket|Etiketi}}]]: $2",
+       "tag-mw-new-redirect": "Serşıkıtışo newe",
+       "tag-mw-blank": "Vengkerdış",
+       "tag-mw-blank-description": "Vengiya na pele bıvurne",
+       "tag-mw-replace": "Zerrek vurriya",
+       "tag-mw-rollback": "Peyserardış",
+       "tag-mw-undo": "Peyser bıgêrê",
        "tags-title": "Etiketi",
        "tags-intro": "Ena pele etiketê ke be vurnayışê nuşiyayışi ra nişan biyê û maneyê inan lista kena.",
        "tags-tag": "Nameyê etiketi",
        "tags-actions-header": "Kerdışi",
        "tags-active-yes": "Eya",
        "tags-active-no": "Nê",
-       "tags-source-extension": "Kışta ju dergeneki ra şınasêna",
+       "tags-source-extension": "Terefê nuştebari ra şınasniyeno",
+       "tags-source-manual": "Terefê karberan û botan ra be dest ra gureniyeno",
        "tags-edit": "bıvurne",
        "tags-delete": "bestere",
        "tags-activate": "Aktiv ke",
        "tags-deactivate": "Aktiv mek",
        "tags-hitcount": "$1 {{PLURAL:$1|vurnayış|vurnayışi}}",
+       "tags-create-heading": "Etiketo newe vırazê",
        "tags-create-tag-name": "Nameyê etiketi:",
        "tags-create-reason": "Sebeb:",
        "tags-create-submit": "Vıraze",
+       "tags-delete-title": "Etiketi bestere",
+       "tags-delete-reason": "Sebeb:",
+       "tags-activate-title": "Etiketê aktifi",
        "tags-activate-reason": "Sebeb:",
+       "tags-activate-submit": "Aktif ke",
+       "tags-deactivate-title": "Etiketê ke aktif niyê",
        "tags-deactivate-reason": "Sebeb:",
+       "tags-deactivate-submit": "Aktif niyê",
+       "tags-edit-title": "Etiketan bıvurne",
+       "tags-edit-manage-link": "Etiketan idare kerê",
+       "tags-edit-existing-tags": "Etiketê ke estê:",
+       "tags-edit-existing-tags-none": "<em>Qet yew</em>",
+       "tags-edit-new-tags": "Etiketê neweyi:",
+       "tags-edit-add": "Nê etiketan cı kerê:",
        "tags-edit-reason": "Sebeb:",
        "comparepages": "Pelan têversanê",
        "compare-page1": "Pele 1",
        "compare-title-not-exists": "Sernameyo ke şımayê vanê mewcud niyo.",
        "compare-revision-not-exists": "Revizyono ke şımaye vanê mewcud niyo.",
        "diff-form": "Ferqi",
+       "diff-form-submit": "Ferqan bımocne",
        "permanentlink": "Gıreyo daimi",
+       "permanentlink-revid": "Revizyonê IDyi",
+       "permanentlink-submit": "Şo revizyoni",
        "dberr-problems": "Mayê muxulêm! Ena sita dı newke xırabiya teknik esta.",
        "dberr-again": "Dı-rê deqiqeyi vınde û heni bar ke.",
        "dberr-info": "(Erzmelumati ra xızmetkari nêreseno: $1)",
        "htmlform-chosen-placeholder": "Opsiyon weçine",
        "htmlform-cloner-create": "Tayêna cı ke",
        "htmlform-cloner-delete": "Wedare",
+       "htmlform-date-placeholder": "SSSS-AA-RR",
+       "htmlform-time-placeholder": "SS:DD:SS",
+       "htmlform-datetime-placeholder": "SSSS-AA-RR SS:DD:SS",
        "logentry-delete-delete": "$1 perra $3 {{GENDER:$2|esterıte}}",
        "logentry-delete-restore": "$1 pela $3 ($4) {{GENDER:$2|peyser arde}}",
        "restore-count-files": "{{PLURAL:$1|1 dosya|$1 dosyeyi}}",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 bayt|$1 bayti}} ($2; $3%)",
        "mediastatistics-table-mimetype": "Tewrê MIME",
        "mediastatistics-header-unknown": "Nêzanaye",
+       "mediastatistics-header-audio": "Veng",
+       "mediastatistics-header-video": "Videoyi",
+       "mediastatistics-header-multimedia": "Medyaya dewlemende",
+       "mediastatistics-header-office": "Ofis",
+       "mediastatistics-header-text": "Tewrê metıni",
+       "mediastatistics-header-total": "Dosyeyi pêro",
        "special-characters-group-latin": "Latin",
        "special-characters-group-latinextended": "latinkiya hêrabiyaye",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Semboli",
        "special-characters-group-greek": "Yunan",
+       "special-characters-group-greekextended": "Yunankiyo herakerde",
        "special-characters-group-cyrillic": "Kiril",
        "special-characters-group-arabic": "Erebki",
        "special-characters-group-arabicextended": "Erebkiya Heraye",
        "special-characters-group-thai": "Thai",
        "special-characters-group-lao": "Lao",
        "special-characters-group-khmer": "Khmer",
+       "special-characters-group-canadianaboriginal": "Aborcinê Kanada",
        "special-characters-title-endash": "tira kılme",
        "special-characters-title-emdash": "tira derge",
        "special-characters-title-minus": "işaretê kemiye",
        "mw-widgets-dateinput-placeholder-day": "SSSS-AA-RR",
        "mw-widgets-dateinput-placeholder-month": "SSSS-AA",
        "mw-widgets-titleinput-description-redirect": "berd be $1",
-       "randomrootpage": "Pela raştmameya rıçıkıne",
+       "mw-widgets-usersmultiselect-placeholder": "Tayêna cı ke...",
+       "mw-widgets-titlesmultiselect-placeholder": "Tayêna cı ke...",
+       "randomrootpage": "Pela raştameya rıçıkıne",
        "log-action-filter-block": "Tipê kılitkerdışi:",
        "log-action-filter-newusers": "Babetê hesabvıraştışi:",
        "log-action-filter-all": "Pêro",
        "log-action-filter-block-block": "Kılitkerdış",
+       "authprovider-resetpass-skip-label": "Bıvêre",
+       "authprovider-resetpass-skip-help": "Peysereştışê parola ra bıvêre.",
+       "authform-notoken": "Tokeno kemi",
+       "authform-wrongtoken": "Nişano ğelet",
        "changecredentials": "Malumatanê karberi bıvurnê",
        "removecredentials": "Kamiye wedarne",
-       "removecredentials-submit": "Kamiyer wedarne"
+       "removecredentials-submit": "Kamiyer wedarne",
+       "revid": "Revizyonê $1",
+       "pageid": "IDyê pela $1",
+       "gotointerwiki": "{{SITENAME}} ra abırriyeno",
+       "passwordpolicies-group": "Grube",
+       "passwordpolicies-policies": "Politikeyi",
+       "passwordpolicies-policy-minimalpasswordlength": "Parola gani tewr senık be {{PLURAL:$1|1 karakter|$1 karakteran}} derg bo"
 }
index e86c6fa..81c305f 100644 (file)
        "ipb_expiry_old": "Expiry time is in the past.",
        "ipb_expiry_temp": "Hidden username blocks must be permanent.",
        "ipb_hide_invalid": "Unable to suppress this account; it has more than {{PLURAL:$1|one edit|$1 edits}}.",
+       "ipb_hide_partial": "Hidden username blocks must be sitewide blocks.",
        "ipb_already_blocked": "\"$1\" is already blocked.",
        "ipb-needreblock": "$1 is already blocked. Do you want to change the settings?",
        "ipb-otherblocks-header": "Other {{PLURAL:$1|block|blocks}}",
index 6c2a931..de6344d 100644 (file)
                        "LittlePuppers",
                        "Theklan",
                        "Laura Ospina",
-                       "Pipino-pumuki"
+                       "Pipino-pumuki",
+                       "Carlosmg.dg"
                ]
        },
        "tog-underline": "Enlaces a subrayar:",
        "pageswithprop-prophidden-binary": "valor de la propiedad binaria oculta ($1)",
        "doubleredirects": "Redirecciones dobles",
        "doubleredirectstext": "Esta página contiene una lista de páginas que redirigen a otras páginas de redirección.\nCada fila contiene enlaces a la segunda y tercera redirección, así como la primera línea de la segunda redirección, en la que usualmente se encontrará el artículo «real» al que la primera redirección debería apuntar.\nLas entradas <del>tachadas</del> han sido resueltas.",
-       "double-redirect-fixed-move": "[[$1]] se ha trasladado.\nSe actualizó automáticamente y ahora redirecciona a [[$2]].",
+       "double-redirect-fixed-move": "[[$1]] se ha trasladado.\nSe actualizó automáticamente y ahora redirige a [[$2]].",
        "double-redirect-fixed-maintenance": "Corrección automática de redirección doble de [[$1]] a [[$2]] mediante una tarea de mantenimiento",
        "double-redirect-fixer": "Corrector de redirecciones",
        "brokenredirects": "Redirecciones incorrectas",
        "listgrouprights-namespaceprotection-namespace": "Espacio de nombres",
        "listgrouprights-namespaceprotection-restrictedto": "Derechos de usuario para editar",
        "listgrants": "Concesiones",
-       "listgrants-summary": "La siguiente es una lista de concesiones con sus permisos de usuario asociados. Los usuarios pueden autorizar aplicaciones para que usen sus cuentas, pero con permisos limitados basados en las concesiones que el usuario le dio a la aplicación. De todas formas, una aplicación actuando a nombre de un usuario no puede hacer uso de permisos que el usuario no posea.\nPuede haber [[{{MediaWiki:Listgrouprights-helppage}}|información adicional]] sobre permisos individuales.",
-       "listgrants-grant": "Conceder",
+       "listgrants-summary": "La siguiente es una lista de concesiones con sus permisos de usuario asociados. Los usuarios pueden autorizar aplicaciones para que usen sus cuentas, pero con permisos limitados basados en las concesiones que el usuario le dio a la aplicación. De todas formas, una aplicación que actúe en nombre de un usuario no puede hacer uso de permisos que el usuario no posea.\nPuede haber [[{{MediaWiki:Listgrouprights-helppage}}|información adicional]] sobre permisos individuales.",
+       "listgrants-grant": "Concesión",
        "listgrants-rights": "Derechos",
        "trackingcategories": "Categorías de seguimiento",
        "trackingcategories-summary": "Esta página lista categorías de seguimiento que han sido generadas automáticamente por el software MediaWiki. Sus nombres pueden cambiarse editando su mensaje correspondiente en el espacio de nombres {{ns:8}}.",
        "ipb-sitewide": "En todo el sitio",
        "ipb-partial": "Parcial",
        "ipb-pages-label": "Páginas",
+       "ipb-namespaces-label": "Espacios de nombres",
        "badipaddress": "La dirección IP no tiene el formato correcto.",
        "blockipsuccesssub": "Bloqueo realizado con éxito",
        "blockipsuccesstext": "\"[[Special:Contributions/$1|$1]]\" ha sido bloqueado.<br />\nVéase la [[Special:BlockList|lista de bloqueos]] para revisarlo.",
        "blocklist-nousertalk": "no puede editar su propia página de discusión",
        "blocklist-editing": "editando",
        "blocklist-editing-sitewide": "edición (en todo el sitio)",
+       "blocklist-editing-page": "páginas",
+       "blocklist-editing-ns": "Espacios de nombres",
        "ipblocklist-empty": "La lista de bloqueos está vacía.",
        "ipblocklist-no-results": "El nombre de usuario o IP indicado no está bloqueado.",
        "blocklink": "bloquear",
        "version-libraries-license": "Licencia",
        "version-libraries-description": "Descripción",
        "version-libraries-authors": "Autores",
-       "redirect": "Redirigir por archivo o por identificador de usuario, página, revisión o registro.",
+       "redirect": "Redirigir por archivo o por identificador de usuario, página, revisión o registro",
        "redirect-summary": "Esta página especial redirige a un archivo (dado un nombre), a una página (dado un identificador de revisión o de página), a una página de usuario (dado un identificador numérico de usuario) o a una entrada del registro (dado un identificador de registro). Uso: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]] o [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Ir",
        "redirect-lookup": "Buscar:",
index d36648e..5f5cc2c 100644 (file)
        "pagecategories": "{{PLURAL:$1|Kategooria|Kategooriad}}",
        "category_header": "Leheküljed kategoorias \"$1\"",
        "subcategories": "Alamkategooriad",
-       "category-media-header": "Meediafailid kategoorias \"$1\"",
-       "category-empty": "''Selles kategoorias pole ühtegi lehekülge ega meediafaili.''",
+       "category-media-header": "Meediumifailid kategoorias \"$1\"",
+       "category-empty": "<em>Selles kategoorias pole praegu ühtegi lehekülge ega meediumifaili.</em>",
        "hidden-categories": "{{PLURAL:$1|Peidetud kategooria|Peidetud kategooriad}}",
        "hidden-category-category": "Peidetud kategooriad",
        "category-subcat-count": "{{PLURAL:$2|Selles kategoorias on ainult järgmine alamkategooria.|{{PLURAL:$1|Järgmine alamkategooria|Järgmised $1 alamkategooriat}} on selles kategoorias (kokku $2).}}",
        "returnto": "Naase lehele $1",
        "tagline": "Allikas: {{SITENAME}}",
        "help": "Juhend",
+       "help-mediawiki": "MediaWiki abiteave",
        "search": "Otsing",
        "search-ignored-headings": " #<!-- Jäta see rida muutmata kujule. --> <pre>\n# Pealkirjad, mida otsingus eiratakse.\n# Muudatused jõustuvad kohe, kui vastava pealkirjaga lehekülg on indekseeritud.\n# Saad teha tühimuudatuse, selleks et sundida lehekülg uuesti indekseerimisele.\n# Süntaks on järgmine:\n#   * Kõik alates märgist \"#\" kuni rea lõpuni on kommentaar.\n#   * Iga rida, mis ei ole tühi, on eiratava pealkirja täpne tõstutundlik kuju.\nViited\nVälislingid\nVaata ka\n #</pre> <!-- Jäta see rida muutmata kujule. -->",
        "searchbutton": "Otsi",
        "sort-ascending": "Järjesta tõusvalt",
        "nstab-main": "Artikkel",
        "nstab-user": "Kasutaja leht",
-       "nstab-media": "Meedia",
+       "nstab-media": "Meediumileht",
        "nstab-special": "Eri",
        "nstab-project": "Projektileht",
        "nstab-image": "Fail",
        "resetpass-abort-generic": "Tarkvaralisa on paroolimuudatuse abortinud.",
        "resetpass-expired": "Sinu parool on iganenud. Palun määra uus parool, et sisse logida.",
        "resetpass-expired-soft": "Sinu parool on iganenud ja tuleb uuesti määrata. Palun vali kohe uus parool või klõpsa nuppu \"{{int:authprovider-resetpass-skip-label}}\", et määrata see hiljem.",
+       "resetpass-validity": "Sinu parool ei ole sobiv: $1\n\nPalun määra uus parool, et sisse logida.",
        "resetpass-validity-soft": "Sinu parool ei sobi: $1\n\nPalun vali kohe uus parool või klõpsa nuppu \"{{int:authprovider-resetpass-skip-label}}\", et see hiljem uuesti määrata.",
        "passwordreset": "Parooli lähtestamine",
        "passwordreset-text-one": "Täida see vorm, et oma parool lähtestada.",
        "blockedtext": "<strong>Sinu kasutajanimi või IP-aadress on blokeeritud.</strong>\n\nBlokeeris $1.\nTema põhjendus on järgmine: <em>$2</em>.\n\n* Blokeeringu algus: $8\n* Blokeeringu lõpp: $6\n* Sooviti blokeerida: $7\n\nKüsimuse arutamiseks võid pöörduda kasutaja $1 või mõne teise [[{{MediaWiki:Grouppage-sysop}}|administraatori]] poole.\n\nSa ei saa kasutada funktsiooni \"{{int:emailuser}}\", kui [[Special:Preferences|konto eelistustes]] pole määratud kehtivat e-posti aadressi või kui sul on keelatud seda funktsiooni kasutada.\n\nSinu praegune IP-aadress on $3 ja blokeeringu number on #$5. Palun lisa need andmed kõigile järelepärimistele, mida kavatsed teha.",
        "autoblockedtext": "Sinu IP-aadress blokeeriti automaatselt, sest seda kasutas teine kasutaja, kelle $1 blokeeris.\nPõhjendus on järgmine:\n\n:<em>$2</em>\n\n* Blokeeringu algus: $8\n* Blokeeringu lõpp: $6\n* Sooviti blokeerida: $7\n\nKüsimuse arutamiseks võid pöörduda kasutaja $1 või mõne teise [[{{MediaWiki:Grouppage-sysop}}|administraatori]] poole.\n\nPane tähele, et sa ei saa kasutada funktsiooni \"{{int:emailuser}}\", kui [[Special:Preferences|konto eelistustes]] pole määratud kehtivat e-posti aadressi või kui sul on keelatud seda funktsiooni kasutada.\n\nSinu praegune IP-aadress on $3 ja blokeeringu number #$5. Palun lisa need andmed kõigile järelpärimistele, mida kavatsed teha.",
        "systemblockedtext": "MediaWiki tarkvara on sinu kasutajanime või IP-aadressi automaatselt blokeerinud.\nToodud on järgmine põhjus:\n\n:<em>$2</em>\n\n* Blokeerimisaeg: $8\n* Blokeeringu aegumistähtaeg: $6\n* Sooviti blokeerida: $7\n\nSinu praegune IP-aadress on $3.\nLisa need andmed kõigile järelepärimistele, mida kavatsed teha.",
+       "actionblockedtext": "Sind on takistatud sooritamast seda toimingut.",
        "blockednoreason": "põhjendust ei ole kirja pandud",
        "whitelistedittext": "Lehekülgede toimetamiseks pead $1.",
        "confirmedittext": "Lehekülgi ei saa toimetada enne e-posti aadressi kinnitamist.\nPalun määra ja kinnita e-posti aadress [[Special:Preferences|eelistuste leheküljel]].",
        "accmailtext": "Kasutajale [[User talk:$1|$1]] genereeritud juhuslik parool saadeti aadressile $2.\n\nSeda saab pärast sisselogimist muuta ''[[Special:ChangePassword|parooli muutmise]]'' leheküljel.",
        "newarticle": "(Uus)",
        "newarticletext": "Lehekülge, kuhu link sind suunas, pole veel.\nEt lehekülg luua, alusta allolevas kastis kirjutamist (lisateave [$1 juhendist]).\nKui sattusid siia kogemata, klõpsa brauseri ''tagasi''-nupule.",
-       "anontalkpagetext": "----''See on anonüümse kasutaja arutelulehekülg. See kasutaja pole kontot loonud või ei kasuta seda. Sellepärast tuleb meil kasutaja tuvastamiseks kasutada tema IP-aadressi. Sellist IP-aadressi võib kasutada mitu kasutajat. Kui oled osutatud IP-aadressi kasutaja ning leiad, et siinsed kommentaarid ei puutu kuidagi sinusse, [[Special:CreateAccount|loo palun kasutajakonto]] või [[Special:UserLogin|logi sisse]], et sind edaspidi teiste anonüümsete kasutajatega segi ei aetaks.''",
+       "anontalkpagetext": "----\n<em>See on anonüümse kasutaja arutelulehekülg. See kasutaja pole kontot loonud või ei kasuta seda.</em>\nSellepärast tuleb meil kasutaja tuvastamiseks kasutada tema IP-aadressi.\nSellist IP-aadressi võib kasutada mitu kasutajat.\nKui oled anonüümne kasutaja ning leiad, et siinsed kommentaarid ei puutu kuidagi sinusse, siis palun [[Special:CreateAccount|loo kasutajakonto]] või [[Special:UserLogin|logi sisse]], et sind ei aetaks edaspidi segi teiste anonüümsete kasutajatega.",
        "noarticletext": "Siin leheküljel puudub praegu tekst.\nSaad [[Special:Search/{{PAGENAME}}|otsida pealkirjateksti]] teistelt lehekülgedelt,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} uurida asjassepuutuvaid logisid]\nvõi [{{fullurl:{{FULLPAGENAME}}|action=edit}} puuduva lehekülje luua]</span>.",
        "noarticletext-nopermission": "Sellel leheküljel pole praegu teksti.\nSaad [[Special:Search/{{PAGENAME}}|otsida selle lehekülje pealkirja]] teistelt lehekülgedelt või <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} otsida seonduvatest logidest]</span>, aga sul pole õigust seda lehekülge alustada.",
        "missing-revision": "Lehekülje \"{{FULLPAGENAME}}\" redaktsiooni $1 pole.\n\nHarilikult tähendab see seda, et sind siia juhatanud link on vananenud ja siin asunud lehekülg on kustutatud.\nÜksikasjad leiad [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} kustutamislogist].",
        "edit-gone-missing": "Polnud võimalik lehekülge uuendada.\nTundub, et see on kustutatud.",
        "edit-conflict": "Redigeerimiskonflikt.",
        "edit-no-change": "Sinu redigeerimist ignoreeriti, sest tekstile ei olnud tehtud muudatusi.",
+       "edit-slots-cannot-add": "Siin puudub {{PLURAL:$1|järgmise pesa|järgmiste pesade}} tugi: $2.",
+       "edit-slots-cannot-remove": "{{PLURAL:$1|Järgmine pesa on nõutav ja seda|Järgmised pesad on nõutavad ja neid}} ei saa eemaldada: $2.",
+       "edit-slots-missing": "{{PLURAL:$1|Järgmine pesa puudub|Järgmised pesad puuduvad}}: $2.",
        "postedit-confirmation-created": "See lehekülg on alustatud.",
        "postedit-confirmation-restored": "See lehekülg on taastatud.",
        "postedit-confirmation-saved": "Sinu muudatus on salvestatud.",
        "defaultmessagetext": "Sõnumi vaiketekst",
        "content-failed-to-parse": "Ebaõnnestus $1i mudeli $2-tüüpi sisu liigendamine: $3",
        "invalid-content-data": "Vigased sisuandmed",
-       "content-not-allowed-here": "Lehekülg [[:$2]] ei tohi sisaldada $1i.",
+       "content-not-allowed-here": "Lehekülje [[:$2]] pesas \"$3\" ei tohi olla $1i.",
        "editwarning-warning": "Sellelt leheküljelt lahkumise tõttu võivad tehtud muudatused kaotsi minna.\nKui oled sisse loginud, saad selle hoiatuse eelistuste alaosas \"{{int:prefs-editing}}\" keelata.",
        "editpage-invalidcontentmodel-title": "Toetamata sisumudel",
        "editpage-invalidcontentmodel-text": "Sisumudelit \"$1\" ei toetata.",
        "editpage-notsupportedcontentformat-title": "Sisuvormingu tugi puudub",
        "editpage-notsupportedcontentformat-text": "Sisumudelil $2 puudub sisuvormingu $1 tugi.",
+       "slot-name-main": "Peamine",
        "content-model-wikitext": "vikitekst",
        "content-model-text": "lihttekst",
        "content-model-javascript": "JavaScript",
        "prefs-watchlist-managetokens": "Halda lube",
        "prefs-misc": "Muu",
        "prefs-resetpass": "Muuda parool",
-       "prefs-changeemail": "muuda e-posti aadressi või eemalda see",
+       "prefs-changeemail": "Muuda e-posti aadressi või eemalda see",
        "prefs-setemail": "Määra e-posti aadress",
        "prefs-email": "E-posti sätted",
        "prefs-rendering": "Ilme",
        "timezonelegend": "Ajavöönd:",
        "localtime": "Kohalik aeg:",
        "timezoneuseserverdefault": "Kasuta serveri vaikesätet ($1)",
-       "timezoneuseoffset": "Muu (määra ajavahe)",
+       "timezoneuseoffset": "Muu (määra allpool ajavahe)",
+       "timezone-useoffset-placeholder": "Näidisväärtused: \"-07:00\" või \"01:00\"",
        "servertime": "Serveri aeg:",
        "guesstimezone": "Loe aeg brauserist",
        "timezoneregion-africa": "Aafrika",
        "right-deletechangetags": "Kustutada andmebaasist [[Special:Tags|märgiseid]]",
        "grant-generic": "Volituse \"$1\" õiguste komplekt",
        "grant-group-page-interaction": "Interaktsioon lehekülgedega",
-       "grant-group-file-interaction": "Interaktsioon meediafailidega",
+       "grant-group-file-interaction": "Interaktsioon meediumifailidega",
        "grant-group-watchlist-interaction": "Interaktsioon sinu jälgimisloendiga",
        "grant-group-email": "E-kirja saatmine",
        "grant-group-high-volume": "Suuremahuline tegevus",
        "grant-delete": "Lehekülgede, redaktsioonide ja logisissekannete kustutamine",
        "grant-editinterface": "MediaWiki nimeruumi ning saidiülese ja kasutaja JSONi redigeerimine",
        "grant-editmycssjs": "Oma CSSi, JSONi või JavaScripti muutmine",
-       "grant-editmyoptions": "Enda eelistuste muutmine",
+       "grant-editmyoptions": "Enda eelistuste ja JSON-häälestuse muutmine",
        "grant-editmywatchlist": "Oma jälgimisloendi muutmine",
        "grant-editsiteconfig": "Saidiülese ning kasutaja CSSi ja JavaScripti muutmine",
        "grant-editpage": "Olemasolevate lehekülgede redigeerimine",
        "rcfilters-watchlist-edit-watchlist-button": "Muuda jälgimisloendit",
        "rcfilters-watchlist-showupdated": "Muudatused lehekülgedel, mida sa pole pärast muudatuste tegemist külastanud, on <strong>rasvases</strong> kirjas ja tähistatud täidetud punktiga.",
        "rcfilters-preference-label": "Kasuta JavaScripti-vaba liidest",
-       "rcfilters-preference-help": "Laadib viimased muudatused ilma filtrite ja esiletõstmise võimaluseta.",
+       "rcfilters-preference-help": "Laadib viimased muudatused ilma filtrite otsimise ja esiletõstmise võimaluseta.",
        "rcfilters-watchlist-preference-label": "Kasuta JavaScripti-vaba liidest",
-       "rcfilters-watchlist-preference-help": "Laadib jälgimisloendi ilma filtrite ja esiletõstmise võimaluseta.",
+       "rcfilters-watchlist-preference-help": "Laadib jälgimisloendi ilma filtrite otsimise ja esiletõstmise võimaluseta.",
        "rcfilters-filter-showlinkedfrom-label": "Näita muudatusi lehekülgedel, millele viidatakse leheküljelt:",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Leheküljed, millele viidatakse</strong> valitud leheküljel",
        "rcfilters-filter-showlinkedto-label": "Näita muudatusi lehekülgedel, millel viidatakse leheküljele",
        "fileexists-thumbnail-yes": "See paistab olevat vähendatud suurusega pilt (''pisipilt''). [[$1|thumb]]\nPalun vaata faili <strong>[[:$1]]</strong>.\nKui vaadatud fail on sama pilt algupärases suuruses, pole vaja täiendavat pisipilti üles laadida.",
        "file-thumbnail-no": "Failinimi algab eesliitega <strong>$1</strong>.\nSee paistab vähendatud suurusega pilt (''pisipilt'') olevat.\nKui sul on ka selle pildi täislahutusega versioon, laadi palun hoopis see üles, vastasel korral muuda palun faili nime.",
        "fileexists-forbidden": "Sellise nimega fail on juba olemas, seda ei saa üle kirjutada.\nPalun pöörduge tagasi ja laadige fail üles mõne teise nime all. [[File:$1|thumb|center|$1]]",
-       "fileexists-shared-forbidden": "Samanimeline fail on juba olemas jagatud meediavaramus.\nKui soovid siiski oma faili üles laadida, siis palun mine tagasi ja kasuta teist failinime.\n[[File:$1|thumb|center|$1]]",
+       "fileexists-shared-forbidden": "Samanimeline fail on juba olemas jagatud failivaramus.\nKui soovid siiski oma faili üles laadida, siis palun mine tagasi ja kasuta teist failinime.\n[[File:$1|thumb|center|$1]]",
        "fileexists-no-change": "Üleslaaditav fail on faili <strong>[[:$1]]</strong> praeguse versiooni üksühene duplikaat.",
        "fileexists-duplicate-version": "Üleslaaditav fail on faili <strong>[[:$1]]</strong> {{PLURAL:$2|vanema versiooni|vanemate versioonide}} üksühene duplikaat.",
        "file-exists-duplicate": "See fail on {{PLURAL:$1|järgmise faili|järgmiste failide}} duplikaat:",
        "move": "Teisalda",
        "movethispage": "Muuda pealkirja",
        "unusedimagestext": "Järgmised failid on olemas, aga pole ühelegi leheküljele lisatud.\nPane tähele, et teised võrgukohad võivad viidata failile otselingiga ja seega võivad siin toodud failid olla ikkagi aktiivses kasutuses.",
+       "unusedimagestext-categorizedimgisused": "Järgmised failid on olemas, aga pole ühelegi leheküljele lisatud. Kategoriseeritud pildid loetakse kasutuses olevaks ka siis, kui neid pole ühelegi leheküljele lisatud.\nPane tähele, et teised võrgukohad võivad viidata failile otselingiga ja seega võivad siin toodud failid olla ikkagi aktiivses kasutuses.",
        "unusedcategoriestext": "Need kategooriad pole ühelgi leheküljel ega teises kategoorias kasutuses.",
        "notargettitle": "Puudub sihtlehekülg",
        "notargettext": "Sa pole määranud selle tegevuse sooritamiseks sihtlehekülge ega kasutajat.",
        "ipbreason": "Põhjus:",
        "ipbreason-dropdown": "*Tavalised blokeerimise põhjused\n** Valeandmete lisamine\n** Lehekülgedelt sisu kustutamine\n** Välislinkide rämpspostitus\n** Sodimine\n** Hirmutav käitumine/ahistamine\n** Mitme konto väärkasutus\n** Lubamatu kasutajanimi",
        "ipb-hardblock": "Keela sellelt IP-aadressilt sisseloginud kasutajatel redigeerida",
-       "ipbcreateaccount": "Takista konto loomist",
-       "ipbemailban": "Takista kasutajal e-kirjade saatmine",
+       "ipbcreateaccount": "Konto loomine",
+       "ipbemailban": "E-kirja saatmine",
        "ipbenableautoblock": "Blokeeri automaatselt viimane IP-aadress, mida see kasutaja kasutas, ja ka järgnevad, mille alt ta võib proovida kaastööd teha",
        "ipbsubmit": "Blokeeri see kasutaja",
        "ipbother": "Muu tähtaeg:",
        "ipboptions": "2 tundi:2 hours,1 päev:1 day,3 päeva:3 days,1 nädal:1 week,2 nädalat:2 weeks,1 kuu:1 month,3 kuud:3 months,6 kuud:6 months,1 aasta:1 year,tähtajatu:infinite",
        "ipbhidename": "Peida kasutajatunnus muudatustest ja loenditest",
        "ipbwatchuser": "Jälgi selle kasutaja lehekülge ja arutelu",
-       "ipb-disableusertalk": "Keela sellel kasutajal blokeeringu ajal oma arutelulehekülge redigeerida",
+       "ipb-disableusertalk": "Enda arutelulehekülje redigeerimine",
        "ipb-change-block": "Blokeeri uuesti nende sätete alusel",
        "ipb-confirm": "Kinnita blokeering",
        "ipb-sitewide": "Saidiülene",
        "ipb-partial": "Osaline",
        "ipb-pages-label": "Leheküljed",
+       "ipb-namespaces-label": "Nimeruumid",
        "badipaddress": "Vigane IP-aadress",
        "blockipsuccesssub": "Blokeerimine õnnestus",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] on blokeeritud.<br />\nKehtivaid blokeeringuid vaata [[Special:BlockList|blokeerimisnimekirjast]].",
        "ipb-blocklist": "Vaata kehtivaid blokeeringuid",
        "ipb-blocklist-contribs": "Kasutaja $1 kaastöö",
        "ipb-blocklist-duration-left": "$1 järel",
+       "block-actions": "Takistatavad toimingud:",
        "block-expiry": "Aegumistähtaeg:",
+       "block-options": "Lisasuvandid:",
+       "block-prevent-edit": "Redigeerimine",
+       "block-reason": "Põhjus:",
+       "block-target": "Kasutajanimi või IP-aadress:",
        "unblockip": "Blokeerimise eemaldamine",
        "unblockiptext": "Kasuta allpool olevat vormi varem blokeeritud IP-aadressi või kasutaja redigeerimisõiguse taastamiseks.",
        "ipusubmit": "Eemalda see blokeering",
        "blocklist-nousertalk": "ei saa oma arutelulehte muuta",
        "blocklist-editing": "redigeerimine",
        "blocklist-editing-sitewide": "redigeerimine (saidiülene)",
+       "blocklist-editing-page": "leheküljed",
+       "blocklist-editing-ns": "nimeruumid",
        "ipblocklist-empty": "Blokeerimiste loend on tühi.",
        "ipblocklist-no-results": "Nõutud IP-aadress või kasutajatunnus ei ole blokeeritud.",
        "blocklink": "blokeeri",
        "tooltip-t-permalink": "Püsilink lehekülje sellele redaktsioonile",
        "tooltip-ca-nstab-main": "Vaata sisulehekülge",
        "tooltip-ca-nstab-user": "Näita kasutaja lehte",
-       "tooltip-ca-nstab-media": "Vaata meediafaili lehekülge",
+       "tooltip-ca-nstab-media": "Vaata meediumifaili lehekülge",
        "tooltip-ca-nstab-special": "See on erilehekülg ja seda ei saa redigeerida.",
        "tooltip-ca-nstab-project": "Näita projekti lehte",
        "tooltip-ca-nstab-image": "Vaata faili lehekülge",
        "pageinfo-display-title": "Kuvatav pealkiri",
        "pageinfo-default-sort": "Vaikejärjestusvõti",
        "pageinfo-length": "Lehekülje pikkus (baitides)",
+       "pageinfo-namespace": "Nimeruum",
        "pageinfo-article-id": "Lehekülje identifikaator",
        "pageinfo-language": "Lehekülje sisu keel",
        "pageinfo-language-change": "muuda",
        "version-editors": "Toimetid",
        "version-antispam": "Rämpsposti tõkestus",
        "version-other": "Muu",
-       "version-mediahandlers": "Meediatöötlejad",
+       "version-mediahandlers": "Meediumitöötlejad",
        "version-hooks": "Haagid",
        "version-parser-extensiontags": "Parseri lisasildid",
        "version-parser-function-hooks": "Parserifunktsioonid",
        "specialpages-group-developer": "Arendusriistad",
        "blankpage": "Tühi leht",
        "intentionallyblankpage": "See lehekülg on sihilikult tühjaks jäetud.",
+       "disabledspecialpage-disabled": "Süsteemiadministraator on selle lehekülje keelanud.",
        "external_image_whitelist": "  #Jäta see rida muutmata kujule<pre>\n#Pane regulaaravaldise osad (vaid //-märkide vahel olev osa) allapoole\n#Need on vastavuses vikiväliste piltide internetiaadressidega\n#Vastavuses olevad kuvatakse piltidena, muul juhul kuvatakse ainult pildi link\n#Märgiga # algavad read on kommentaarid\n#See on tõstutundetu\n\n#Pane kõik regulaaravaldise osad selle joone kohale. Jäta see rida muutmata kujule</pre>",
        "tags": "Käibivad muudatusmärgised",
        "tag-filter": "[[Special:Tags|Märgisefilter]]:",
        "logentry-block-block": "$1 {{GENDER:$2|blokeeris}} kasutaja {{GENDER:$4|$3}}; aegumistähtaeg $5 $6",
        "logentry-block-unblock": "$1 {{GENDER:$2|eemaldas}} kasutaja {{GENDER:$4|$3}} blokeeringu",
        "logentry-block-reblock": "$1 {{GENDER:$2|muutis}} kasutaja {{GENDER:$4|$3}} blokeeringut; aegumistähtaeg $5 $6",
-       "logentry-partialblock-block": "$1 {{GENDER:$2|blokeeris}} kasutaja {{GENDER:$4|$3}} redigeerimast {{PLURAL:$8|lehekülge|lehekülgi}} $7; aegumistähtaeg $5 $6",
-       "logentry-partialblock-reblock": "$1 {{GENDER:$2|muutis}} kasutaja {{GENDER:$4|$3}} blokeeringut, mis takistab redigeerimist {{PLURAL:$8|leheküljel|lehekülgedel}} $7; aegumistähtaeg $5 $6",
+       "logentry-partialblock-block-page": "{{PLURAL:$1|lehekülge|lehekülgi}} $2",
+       "logentry-partialblock-block-ns": "{{PLURAL:$1|nimeruumis|nimeruumides}} $2",
+       "logentry-partialblock-block": "$1 {{GENDER:$2|blokeeris}} kasutaja {{GENDER:$4|$3}} redigeerimast $7; aegumistähtaeg $5 $6",
+       "logentry-partialblock-reblock": "$1 {{GENDER:$2|muutis}} kasutaja {{GENDER:$4|$3}} blokeeringut, mis takistab redigeerimist $7; aegumistähtaeg $5 $6",
        "logentry-non-editing-block-block": "$1 {{GENDER:$2|blokeeris}} kasutaja {{GENDER:$4|$3}} tegemast määratud tegevusi peale redigeerimise; aegumistähtaeg $5 $6",
        "logentry-non-editing-block-reblock": "$1 {{GENDER:$2|muutis}} kasutaja {{GENDER:$4|$3}} blokeeringut, mis takistab tegemast määratud tegevusi peale redigeerimise; aegumistähtaeg $5 $6",
        "logentry-suppress-block": "$1 {{GENDER:$2|blokeeris}} kasutaja {{GENDER:$4|$3}}; aegumistähtaeg $5 $6",
        "default-skin-not-found-no-skins": "Oih! Sinu viki vaikekujundus, milleks muutuja <code dir=\"ltr\">$wgDefaultSkin</code> järgi on <code>$1</code>, pole saadaval.\n\nÜhtegi kujundust pole paigaldatud.\n\n; Kui oled MediaWiki just paigaldanud või täiendasid seda:\n: Paigaldasid tarkvara ilmselt Giti kaudu või otse lähtekoodist või mõnel muul viisil. See on ootuspärane. MediaWiki 1.24 ja uuemad versioonid ei sisalda peahoidlas ühtegi kujundust. Proovi [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org-i kujunduste kataloogist] mõni kujundus paigaldada. Selleks saad:\n:* laadida alla [https://www.mediawiki.org/wiki/Download lintarhiivi paigaldaja], mis sisaldab mitut kujundust ja tarkvaralisa. Saad sealt kleepimiseks kopeerida kausta <code dir=\"ltr\">skins/</code>;\n:* [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org-ist] kindla kujunduse lintarhiivi alla laadida;\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins kasutada Giti, et kujundusi alla laadida].\n: Selle tegemine ei tohiks häirida Giti hoidlat, kui oled MediaWiki arendaja. Vaata [https://www.mediawiki.org/wiki/Manual:Skin_configuration kujunduste häälestusjuhendist], kuidas kujundusi lubada ja kuidas valida vaikekujundus.",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (lubatud)",
        "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 (<strong>keelatud</strong>)",
-       "mediastatistics": "Meediafailide arvandmestik",
+       "mediastatistics": "Meediumifailide arvandmestik",
        "mediastatistics-summary": "Arvandmed üles laaditud failitüüpide kohta. See käib ainult failide viimaste versioonide kohta. Vanu ja kustutatud versioone pole arvesse võetud.",
        "mediastatistics-nbytes": "{{PLURAL:$1|$1 bait|$1 baiti}} ($2; $3%)",
        "mediastatistics-bytespertype": "Failide kogusuurus selles alaosas: $1 {{PLURAL:$1|bait|baiti}} ($2; $3%).",
        "mw-widgets-dateinput-no-date": "Kuupäev valimata",
        "mw-widgets-dateinput-placeholder-day": "AAAA-KK-PP",
        "mw-widgets-dateinput-placeholder-month": "AAAA-KK",
-       "mw-widgets-mediasearch-input-placeholder": "Otsi meediafaile",
+       "mw-widgets-mediasearch-input-placeholder": "Otsi meediumifaile",
        "mw-widgets-mediasearch-noresults": "Tulemusi ei leitud.",
        "mw-widgets-titleinput-description-new-page": "lehekülge pole veel",
        "mw-widgets-titleinput-description-redirect": "ümbersuunamine leheküljele \"$1\"",
index 274dada..69fba10 100644 (file)
        "modifiedarticleprotection": "\"[[$1]]\"(r)en babes maila aldatu da",
        "unprotectedarticle": "\"[[$1]]\"-(r)i babesa kendu zaio",
        "movedarticleprotection": "babes hobespenak «[[$2]]» orritik «[[$1]]» orrira aldatu dira",
-       "protectedarticle-comment": "{{GENDER:$2|Babestua}} \"[[$1]]\"",
+       "protectedarticle-comment": "administratzaileak «[[$1]]» {{GENDER:$2|babestu du}}",
        "modifiedarticleprotection-comment": "{{GENDER:$2|Babespen maila aldatu du}} \"[[$1]]\"(r)entzako",
        "unprotectedarticle-comment": "{{GENDER:$2|administratzaileak}} babesa kendu dio «[[$1]]» orriari",
        "protect-title": "«$1» babesten",
        "ipb-blocklist": "Blokeaketak ikusi",
        "ipb-blocklist-contribs": "{{GENDER:$1|$1(r)en}} ekarpenak",
        "ipb-blocklist-duration-left": "gainerako $1",
+       "block-actions": "Blokeatuko diren ekintzak:",
        "block-expiry": "Iraungipena",
        "unblockip": "Erabiltzailea desblokeatu",
        "unblockiptext": "Erabili beheko formularioa lehenago blokeatutako IP helbide edo erabiltzaile baten idazketa baimenak leheneratzeko.",
        "createaccountblock": "kontua sortzea blokeatuta",
        "emailblock": "e-posta blokeatuta",
        "blocklist-nousertalk": "zure buruaren eztabaida orrialdea ezin duzu aldatu",
+       "blocklist-editing": "aldatzen",
+       "blocklist-editing-sitewide": "editatzea (gune osoan)",
        "ipblocklist-empty": "Blokeaketa zerrenda hutsik dago.",
        "ipblocklist-no-results": "Zehaztutako IP helbide edo erabiltzaile izena ez dago blokeatuta.",
        "blocklink": "blokeatu",
index 8d1b1e9..352efe1 100644 (file)
        "exif-dc-type": "نوع  الوسيط",
        "exif-rating-rejected": "مرفوض",
        "exif-isospeedratings-overflow": "أكبر من 65535",
+       "exif-maxaperturevalue-value": "$1 أبكس (f/$2)",
        "exif-iimcategory-ace": "فنون وثقافة وترفيه",
        "exif-iimcategory-clj": "جريمة وقانون",
        "exif-iimcategory-dis": "كوارث وحوادث",
index bcf0e74..5b9cbcd 100644 (file)
        "exif-fixtureidentifier": "Tugiandmete nimi",
        "exif-locationdest": "Kujutatud asukoht",
        "exif-locationdestcode": "Kujutatud asukoha kood",
-       "exif-objectcycle": "Päeva osa, milleks meediafail on ette nähtud",
+       "exif-objectcycle": "Päeva osa, milleks meediumifail on ette nähtud",
        "exif-contact": "Kontaktteave",
        "exif-writer": "Autor",
        "exif-languagecode": "Keel",
        "exif-dc-relation": "Seotud teabevahendid",
        "exif-dc-rights": "Õigused",
        "exif-dc-source": "Allikmeediafail",
-       "exif-dc-type": "Meediafaili tüüp",
+       "exif-dc-type": "Meediumitüüp",
        "exif-rating-rejected": "Tagasi lükatud",
        "exif-isospeedratings-overflow": "Suurem kui 65535",
        "exif-iimcategory-ace": "Kunstid, kultuur ja meelelahutus",
index 4b361b4..fa028be 100644 (file)
        "exif-gpsdop": "Nákvæmni mælinga",
        "exif-gpsspeedref": "Hraðaeining",
        "exif-gpsspeed": "Hraði GPS-móttakara",
+       "exif-gpstrackref": "Tilvísun í stefnu hreyfingar",
        "exif-gpstrack": "Átt hreyfingar",
+       "exif-gpsimgdirectionref": "Tilvísun fyrir stefnu myndar",
        "exif-gpsimgdirection": "Stefna myndarinnar",
        "exif-gpsmapdatum": "Landmælingagögn",
        "exif-gpsdestlatituderef": "Tilvísun breiddargráðu áfangastaðar",
index 23d6235..d9c1fee 100644 (file)
@@ -7,6 +7,7 @@
        },
        "exif-orientation": "ⴰⵙⵡⴰⵍⴰ",
        "exif-datetime": "ⴰⵙⴰⴽⵓⴷ ⴷ ⵡⴰⴽⵓⴷ ⵏ ⵓⵙⵏⴼⵍ ⵏ ⵓⴼⴰⵢⵍⵓ",
+       "exif-datetimedigitized": "ⴰⵙⴰⴽⵓⴷ ⴷ ⵡⴰⴽⵓⴷ ⵏ ⵓⵙⵓⵟⵟⵏ",
        "exif-flash": "ⴼⵍⴰⵛ",
        "exif-source": "ⴰⵙⴰⴳⵎ",
        "exif-languagecode": "ⵜⵓⵜⵍⴰⵢⵜ",
index 907176f..25eae75 100644 (file)
        "nosuchusershort": "Käyttäjää nimeltä ”$1” ei ole. Kirjoititko nimen oikein?",
        "nouserspecified": "Käyttäjätunnusta ei ole määritelty.",
        "login-userblocked": "Tämä käyttäjä on estetty. Kirjautuminen ei ole sallittua.",
-       "wrongpassword": "Virheellinen käyttäjätunnus tai salasana.\nOle hyvä ja yritä uudelleen.",
+       "wrongpassword": "Virheellinen käyttäjänimi tai salasana.\nOle hyvä ja yritä uudelleen.",
        "wrongpasswordempty": "Et voi antaa tyhjää salasanaa.",
        "passwordtooshort": "Salasanan täytyy olla vähintään {{PLURAL:$1|yhden merkin pituinen|$1 merkkiä pitkä}}.",
        "passwordtoolong": "Salasanat saavat olla enintään $1 {{PLURAL:$1|merkin}} pituisia.",
        "contentmodelediterror": "Et voi muokata tätä versiota, koska sen sisältömalli on <code>$1</code> ja tämä poikkeaa sivun nykyisestä sisältömallista, joka on <code>$2</code>.",
        "recreate-moveddeleted-warn": "'''Varoitus: Olet luomassa sellaista sivua, joka on aikaisemmin poistettu.'''\n\nHarkitse, kannattaako tätä sivua luoda uudelleen. \nAlla on tämän sivun poisto- ja siirtohistoria:",
        "moveddeleted-notice": "Tämä sivu on poistettu. \nAlla on sivun poisto-, suojaus- ja siirtohistoria.",
-       "moveddeleted-notice-recent": "Valitettavasti tämä sivu on poistettu äskettäin (viimeisen 24 tunnin aikana).\nAlla on sivun poisto-, suojaus- ja siirtohistoria.",
+       "moveddeleted-notice-recent": "Tämä sivu on poistettu äskettäin (viimeisen 24 tunnin aikana).\nAlla on sivun poisto-, suojaus- ja siirtohistoria.",
        "log-fulllog": "Näytä loki kokonaan",
        "edit-hook-aborted": "Laajennuskoodi esti muokkauksen antamatta syytä.",
        "edit-gone-missing": "Sivun päivitys ei onnistunut.\nSe on ilmeisesti poistettu.",
        "protect-text": "Voit tarkastella ja muuttaa sivun '''$1''' suojaustasoa.",
        "protect-locked-blocked": "Et voi muuttaa sivun suojauksia, koska sinut on estetty. Alla on sivun ”'''$1'''” nykyiset suojaukset:",
        "protect-locked-dblock": "Sivun suojauksia ei voi muuttaa, koska tietokanta on lukittu. Alla on sivun ”'''$1'''” nykyiset suojaukset:",
-       "protect-locked-access": "Sinun käyttäjätunnuksellasi ei ole oikeutta muuttaa sivujen suojauksia. \nTässä ovat nykyiset suojausasetukset sivulla <strong>$1</strong>:",
+       "protect-locked-access": "Käyttäjätunnuksellasi ei ole oikeutta muuttaa sivujen suojauksia. \nTässä ovat nykyiset suojausasetukset sivulla <strong>$1</strong>:",
        "protect-cascadeon": "Tämä sivu on suojauksen kohteena, koska se on sisällytetty alla {{PLURAL:$1|olevaan suojattuun sivuun, jossa|oleviin suojattuihin sivuihin, joissa}} on kytketty tarttuva suojaus päälle.\n\nTämän sivun suojaustasoon tehdyillä muutoksilla ei ole vaikutusta sivuun muualta tarttuneeseen suojaukseen.",
        "protect-default": "Salli kaikki käyttäjät",
        "protect-fallback": "Salli vain käyttäjät, joilla on oikeus $1",
index ea4fc68..c771a6c 100644 (file)
                        "Ash Crow",
                        "Alacabe",
                        "Eihel",
-                       "Tektasc"
+                       "Tektasc",
+                       "DSwissK"
                ]
        },
        "tog-underline": "Soulignement des liens :",
        "prefs-help-gender": "La définition de cette préférence est facultative.\nLe logiciel utilise cette valeur pour s’adresser à vous ou pour faire mention de vous aux autres en utilisant le bon genre grammatical.\nCette information sera publique.",
        "email": "Courriel",
        "prefs-help-realname": "Le vrai nom est facultatif.\nS’il est fourni, il sera utilisé pour vous attribuer vos contributions.",
-       "prefs-help-email": "L'adresse de courriel est facultative, mais elle est nécessaire pour réinitialiser votre mot de passe, si vous veniez à l'oublier.",
+       "prefs-help-email": "L'adresse de courriel est facultative, mais elle est nécessaire pour réinitialiser votre mot de passe, en cas d'oubli.",
        "prefs-help-email-others": "Vous pouvez aussi choisir de laisser les autres vous contacter par courriel via un lien sur votre page de discussion ou page utilisateur. \nVotre adresse courriel n'est pas révélée quand les autres utilisateurs vous contactent.",
        "prefs-help-email-required": "Une adresse de courriel est requise.",
        "prefs-info": "Informations de base",
        "ipb_expiry_old": "L’heure d’expiration est passée.",
        "ipb_expiry_temp": "Les blocages de noms d'utilisateurs cachés doivent être permanents.",
        "ipb_hide_invalid": "Impossible de supprimer ce compte ; il semble avoir plus {{PLURAL:$1|d’une modification|de $1 modifications}}.",
+       "ipb_hide_partial": "Les blocages de nom d’utilisateur masqués doivent être des blocages pour le site.",
        "ipb_already_blocked": "« $1 » est déjà bloqué",
        "ipb-needreblock": "$1 est déjà bloqué. Voulez-vous modifier les paramètres ?",
        "ipb-otherblocks-header": "{{PLURAL:$1|Autre blocage|Autres blocages}}",
        "watchlistedit-clear-titles": "Titres :",
        "watchlistedit-clear-submit": "Effacer la liste de suivi (ceci est permanent !)",
        "watchlistedit-clear-done": "Votre liste de suivi a été effacée.",
-       "watchlistedit-clear-jobqueue": "Votre liste de suivi est en cours de supression. Ce qui peut prendre un certain temps.",
+       "watchlistedit-clear-jobqueue": "Votre liste de suivi est en cours de suppression. Ce qui peut prendre un certain temps.",
        "watchlistedit-clear-removed": "{{PLURAL:$1|Un titre a été|$1 titres ont été}} retirés :",
        "watchlistedit-too-many": "Il y a trop de pages à afficher ici.",
        "watchlisttools-clear": "Effacer la liste de suivi",
index 657e641..dd7ba71 100644 (file)
        "specialpage": "विशेश पान",
        "personaltools": "खाजगी साधनां",
        "talk": "भासाभास",
-       "views": "दà¥\83शà¥\8dयाà¤\82",
+       "views": "दà¥\83षà¥\8dà¤\9fà¥\80",
        "toolbox": "साधनां",
        "imagepage": "फायलीचें पान पळेयात",
        "mediawikipage": "संदेशाचें पान पळयात",
index 8315bcb..ce64a0a 100644 (file)
@@ -17,7 +17,7 @@
        "tog-enotifwatchlistpages": "Mhojea sadurvollerintlem pan vo fayl bodol'li zalear mhaka email dhadd",
        "tog-shownumberswatching": "Nodor dovorpi vaporpeanche sonkhya dakhoi",
        "tog-oldsig": "Tujea sod'dheachi soy:",
-       "tog-uselivepreview": "Boroitastana zolok dahkoi",
+       "tog-uselivepreview": "Pan porot ugdinastana zolok dahkoi",
        "tog-watchlisthideown": "Sadurvollerint mhoje bodol lipoi",
        "tog-watchlisthidebots": "Sadurvollerint robotani kel'le bodol lipoi",
        "tog-watchlisthideminor": "Sadurvollerint dhaktem bodol lipoi",
        "specialpage": "Vixex pan",
        "personaltools": "Khasgi avtam",
        "talk": "Bhasabhas",
-       "views": "Drishya",
+       "views": "Dixtti",
        "toolbox": "Avtam",
        "imagepage": "Faylichem pan poloi",
        "mediawikipage": "Sondexachem pan polloi",
        "watchthis": "Hea panar dixtt dovor",
        "savearticle": "Pan samball",
        "savechanges": "Bodol samball",
+       "publishpage": "Pan uzvaddai",
        "publishchanges": "Bodol uzvaddai",
+       "publishpage-start": "Pan uzvaddai...",
        "publishchanges-start": "Bodol uzvaddai...",
        "preview": "Zholok",
        "showpreview": "Zholok dakhoi",
        "tooltip-ca-nstab-category": "Vorgachem pan polloi",
        "tooltip-minoredit": "Haka ek kirkoll sudharop mhunn khunnay",
        "tooltip-save": "Tuje bodol sambhall",
+       "tooltip-publish": "Tujeo bodol uzvaddai",
        "tooltip-preview": "Bodolanchi zholok polloi. Upkar korun samballche adim hachem upeog kor!",
        "tooltip-diff": "Tumi hea mozkurant kelelo bodol dakhoiat",
        "tooltip-compareselectedversions": "Hea panacheo don nivoddleleo uzollneo modem forok polloi",
index db3479f..41842e4 100644 (file)
        "ipb_expiry_old": "זמן הפקיעה כבר עבר.",
        "ipb_expiry_temp": "חסימות הכוללות הסתרת שם משתמש חייבות להיות לזמן בלתי מוגבל.",
        "ipb_hide_invalid": "לא ניתן להעלים את החשבון הזה; {{PLURAL:$1|בוצעה ממנו יותר מעריכה אחת|בוצעו ממנו יותר מ־$1 עריכות}}.",
+       "ipb_hide_partial": "חסימות הכוללות הסתרת שם משתמש חייבות לחול על כל האתר.",
        "ipb_already_blocked": "המשתמש \"$1\" כבר נחסם.",
        "ipb-needreblock": "$1 כבר {{GENDER:$1|חסום|חסומה}}. האם ברצונך לשנות את הגדרות החסימה?",
        "ipb-otherblocks-header": "{{PLURAL:$1|חסימה אחרת|חסימות אחרות}}",
index 6587878..7e4bcd9 100644 (file)
        "right-purge": "Čišćenje priručne memorije stranice bez stranice za potvrdu",
        "right-autoconfirmed": "Uređivanje stranica zaštićenih za neprijavljene suradnike",
        "right-bot": "Izmjene su tretirane kao automatski proces (bot)",
-       "right-nominornewtalk": "Neposjedovanje manjih izmjena na stranicama za razgovor otvara prozor za nove poruke",
-       "right-apihighlimits": "Korištenje viših granica za upite od izvršnika",
-       "right-writeapi": "Mogućnost zapišavanja u izvršniku",
+       "right-nominornewtalk": "Izbjegavanje prikazivanja obavijesti o novim porukama kad je označeno da je izmjena manja",
+       "right-apihighlimits": "Korištenje viših ograničenja kod API upita",
+       "right-writeapi": "Mogućnost pisanja API-ja",
        "right-delete": "Brisanje stranica",
        "right-bigdelete": "Brisanje stranica koje imaju veliku povijest",
        "right-deletelogentry": "Brisanje i vraćanje određenih zapisa u evidenciji",
        "action-reupload": "postavljanje nove inačice ove datoteke",
        "action-reupload-shared": "postavljanje nove inačice ove datoteke na zajedničkom poslužitelju",
        "action-upload_by_url": "postavljanje ove datoteke preko URL adrese",
-       "action-writeapi": "korištenje zapišavanja u izvršniku",
+       "action-writeapi": "upotreba za pisanje API-jem",
        "action-delete": "brisanje ove stranice",
        "action-deleterevision": "brisanje izmjena",
        "action-deletedhistory": "gledanje obrisane povijesti ove stranice",
index 01138e3..6f53635 100644 (file)
        "resetpass-abort-generic": "Le cambio del contrasigno ha essite abortate per un extension.",
        "resetpass-expired": "Le contrasigno ha expirate. Per favor defini un nove contrasigno pro aperir session.",
        "resetpass-expired-soft": "Le contrasigno ha expirate e debe esser cambiate. Per favor, elige un nove contrasigno ora, o clicca sur \"{{int:authprovider-resetpass-skip-label}}\" pro cambiar lo plus tarde.",
+       "resetpass-validity": "Tu contrasigno non es valide: $1\n\nPer favor defini un nove contrasigno pro aperir session.",
        "resetpass-validity-soft": "Le contrasigno non es valide: $1\n\nPer favor, elige un nove contrasigno ora, o clicca sur \"{{int:authprovider-resetpass-skip-label}}\" pro cambiar lo plus tarde.",
        "passwordreset": "Reinitialisar contrasigno",
        "passwordreset-text-one": "Completa iste formulario pro reinitialisar tu contrasigno.",
        "blockedtext": "<strong>Tu nomine de usator o adresse IP ha essite blocate.</strong>\n\nLe blocada esseva facite per $1.\nLe motivo presentate es <em>$2</em>.\n\n* Initio del blocada: $8\n* Expiration del blocada: $6\n* Le blocato intendite: $7\n\nTu pote contactar $1 o un altere [[{{MediaWiki:Grouppage-sysop}}|administrator]] pro discuter le blocada.\nTu pote solmente usar le function \"{{int:emailuser}}\" si un adresse de e-mail valide es specificate in le\n[[Special:Preferences|preferentias de tu conto]] e tu non ha essite blocate de usar lo.\nTu adresse IP actual es $3, e le ID del blocada es #$5.\nPer favor include tote le detalios supra specificate in omne correspondentia.",
        "autoblockedtext": "Tu adresse IP ha essite automaticamente blocate perque un altere usator lo usava qui esseva blocate per $1.\nLe motivo presentate es:\n\n:<em>$2</em>\n\n* Initio del blocada: $8\n* Expiration del blocada: $6\n* Blocato intendite: $7\n\nTu pote contactar $1 o un del altere [[{{MediaWiki:Grouppage-sysop}}|administratores]] pro discuter le blocada.\n\nNota que tu pote solmente utilisar le function \"{{int:emailuser}}\" si tu ha registrate un adresse de e-mail valide in tu [[Special:Preferences|preferentias de usator]] e tu non ha essite blocate de usar lo.\n\nTu adresse IP actual es $3, e le ID del blocada es #$5.\nPer favor include tote le detalios supra specificate in omne correspondentia.",
        "systemblockedtext": "Tu nomine de usator o adresse IP ha essite blocate automaticamente per MediaWiki.\nLe motivo presentate es:\n\n:<em>$2</em>\n\n* Initio del blocada: $8\n* Expiration del blocada: $6\n* Blocato intendite: $7\n\nTu adresse IP actual es $3.\nPer favor, include tote le detalios enumerate hic supra in omne questiones que tu pone.",
+       "actionblockedtext": "Tu ha essite blocate pro exequer iste action.",
        "blockednoreason": "nulle motivo specificate",
        "whitelistedittext": "Tu debe $1 pro poter modificar paginas.",
        "confirmedittext": "Tu debe confirmar tu adresse de e-mail pro poter modificar paginas.\nPer favor entra e valida tu adresse de e-mail per medio de tu [[Special:Preferences|preferentias de usator]].",
        "ipbreason": "Motivo:",
        "ipbreason-dropdown": "*Motivos frequente pro blocar\n** Insertion de informationes false\n** Elimination de contento de paginas\n** Ligamines ''spam'' verso sitos externe\n** Insertion de nonsenso/absurditates in paginas\n** Comportamento intimidatori/molestation\n** Abuso de contos multiple\n** Nomine de usator inacceptabile",
        "ipb-hardblock": "Impedir que usatores authenticate face modificationes ab iste adresse IP",
-       "ipbcreateaccount": "Impedir creation de contos",
-       "ipbemailban": "Impedir que le usator invia e-mail",
+       "ipbcreateaccount": "Creation de contos",
+       "ipbemailban": "Inviar e-mail",
        "ipbenableautoblock": "Blocar automaticamente le adresse IP usate le plus recentemente per iste usator, e omne IPs successive desde le quales ille/-a tenta facer modificationes",
        "ipbsubmit": "Blocar iste usator",
        "ipbother": "Altere durata:",
        "ipboptions": "2 horas:2 hours,1 die:1 day,3 dies:3 days,1 septimana:1 week,2 septimanas:2 weeks,1 mense:1 month,3 menses:3 months,6 menses:6 months,1 anno:1 year,infinite:infinite",
        "ipbhidename": "Celar le nomine del usator del modificationes e del listas",
        "ipbwatchuser": "Observar le paginas de usator e de discussion de iste usator",
-       "ipb-disableusertalk": "Impedir que iste usator modifica su proprie pagina de discussion durante que ille es blocate",
+       "ipb-disableusertalk": "Modificar le proprie pagina de discussion",
        "ipb-change-block": "Reblocar le usator con iste configurationes",
        "ipb-confirm": "Confirmar blocada",
        "ipb-sitewide": "Sur tote le sito",
        "ipb-partial": "Partial",
        "ipb-pages-label": "Paginas",
+       "ipb-namespaces-label": "Spatios de nomines",
        "badipaddress": "Adresse IP mal formate.",
        "blockipsuccesssub": "Blocada succedite",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] ha essite blocate.<br />\nVide le [[Special:BlockList|lista de blocadas]] pro revider le blocadas.",
        "ipb-blocklist": "Vider blocadas existente",
        "ipb-blocklist-contribs": "Contributiones de {{GENDER:$1|$1}}",
        "ipb-blocklist-duration-left": "$1 restante",
+       "block-actions": "Actiones a blocar:",
        "block-expiry": "Expiration:",
+       "block-options": "Optiones additional:",
+       "block-prevent-edit": "Modification",
+       "block-reason": "Motivo:",
+       "block-target": "Nomine de usator o adresse IP:",
        "unblockip": "Disblocar usator",
        "unblockiptext": "Usa le formulario infra pro restaurar le accesso de scriptura a un adresse IP o nomine de usator blocate previemente.",
        "ipusubmit": "Cancellar iste blocada",
        "blocklist-nousertalk": "non pote modificar su proprie pagina de discussion",
        "blocklist-editing": "modification",
        "blocklist-editing-sitewide": "modification (tote le sito)",
+       "blocklist-editing-page": "paginas",
+       "blocklist-editing-ns": "spatios de nomines",
        "ipblocklist-empty": "Le lista de blocadas es vacue.",
        "ipblocklist-no-results": "Le adresse IP o nomine de usator que tu requestava non es blocate.",
        "blocklink": "blocar",
        "specialpages-group-developer": "Instrumentos pro disveloppatores",
        "blankpage": "Pagina vacue",
        "intentionallyblankpage": "Iste pagina es intentionalmente vacue",
+       "disabledspecialpage-disabled": "Iste pagina ha essite disactivate per un administrator del systema.",
        "external_image_whitelist": "  #Lassa iste linea exactemente como illo es<pre>\n#Pone fragmentos de expressiones regular (solmente le parte que va inter //) infra\n#Istes correspondera con le adresses URL de imagines externe (a ligamine directe)\n#Le correspondentes se monstrara como imagines, le alteres solmente como ligamines a imagines\n#Le lineas comenciante con # essera tractate como commentos\n#Isto non es sensibile al differentia inter majusculas e minusculas\n\n#Insere omne fragmentos regex super iste linea. Lassa iste linea exactemente como illo es</pre>",
        "tags": "Etiquettas valide de modification",
        "tag-filter": "Filtro de [[Special:Tags|etiquettas]]:",
        "logentry-block-block": "$1 {{GENDER:$2|blocava}} {{GENDER:$4|$3}} con un tempore de expiration de $5 $6",
        "logentry-block-unblock": "$1 {{GENDER:$2|disblocava}} {{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1 {{GENDER:$2|cambiava}} le configuration del blocada de {{GENDER:$4|$3}} con un tempore de expiration de $5 $6",
-       "logentry-partialblock-block": "$1 {{GENDER:$2|blocava}} {{GENDER:$4|$3}} pro modificar {{PLURAL:$8||le paginas}} $7 con un tempore de expiration de $5 $6",
-       "logentry-partialblock-reblock": "$1 {{GENDER:$2|cambiava}} le configuration del blocada de {{GENDER:$4|$3}} impediente le modification {{PLURAL:$8|de|del paginas}} $7 con un tempore de expiration de $5 $6",
+       "logentry-partialblock-block-page": "le {{PLURAL:$1|pagina|paginas}} $2",
+       "logentry-partialblock-block-ns": "le {{PLURAL:$1|spatio|spatios}} de nomines $2",
+       "logentry-partialblock-block": "$1 {{GENDER:$2|blocava}} {{GENDER:$4|$3}} pro modificar $7 con un tempore de expiration de $5 $6",
+       "logentry-partialblock-reblock": "$1 {{GENDER:$2|cambiava}} le configuration del blocada de $3 impediente que {{GENDER:$4|ille|illa}} modifica $7 con un tempore de expiration de $5 $6",
        "logentry-non-editing-block-block": "$1 {{GENDER:$2|blocava}} {{GENDER:$4|$3}} pro certe actiones non-modification con un tempore de expiration de $5 $6",
        "logentry-non-editing-block-reblock": "$1 {{GENDER:$2|cambiava}} le configuration del blocada de {{GENDER:$4|$3}} pro certe actiones non-modification con un tempore de expiration de $5 $6",
        "logentry-suppress-block": "$1 {{GENDER:$2|blocava}} {{GENDER:$4|$3}} con un tempore de expiration de $5 $6",
index a624451..5008609 100644 (file)
@@ -4,44 +4,51 @@
                        "Reedy",
                        "Ukabia",
                        "아라",
-                       "Uzoma Ozurumba"
+                       "Uzoma Ozurumba",
+                       "Oby Ezeilo"
                ]
        },
        "tog-underline": "Ahịrịàlà òjikọ:",
        "tog-hideminor": "Zonari orü ntàkírí na nwerue mẹrẹ ogẹ nsó",
        "tog-hidepatrolled": "Zonari orü ha hụrụ na nwerue mẹrẹ ogẹ nsó",
        "tog-newpageshidepatrolled": "Zonari orü ha hụrụ shí ndetu ihü ohúrù",
+       "tog-hidecategorization": "Zoo nkewasị e nwere n' ihu akwụkwọ nke ọbụla",
        "tog-extendwatchlist": "Gbasa ndetu ihe ánà elé ka ó zi gbanwere nke níle, o bughi nani nke isi nso",
-       "tog-usenewrc": "Ji ihe gbanwere nso níguélé élu (ö chọrọ JavaScript)",
+       "tog-usenewrc": "ngbanwe otu na-esite n'ihu akwụkwọ nwere mgbanwe ọhụụ nakwa ihe nrụba ama.",
        "tog-numberheadings": "Onuogụgụ-otu anyi mere ya maka ishi edemede",
-       "tog-editondblclick": "Rüwá na élu ihü mgbe I kpạtạrạ (ö chọrọ JavaScript)",
-       "tog-editsectiononrightclick": "Kpa na áká nri Í gbanyé orürü nkeji na ishi nkeji (ö chọrọ JavaScript)",
-       "tog-watchcreations": "Tinyé ihüm na eké na ndetu ihem ne lé",
-       "tog-watchdefault": "Tinyé ihü m na rüoru élu na ndetu ihem ne lé",
-       "tog-watchmoves": "Tinyé ihü m na puzié na ndetu ihe m ne lé",
-       "tog-watchdeletion": "Tinyé ihü m na kàcha na ndetu ihe m ne lé",
+       "tog-editondblclick": "Hazie ihu akwụkwọ ndi a site na kliki ugboro abụọ",
+       "tog-editsectiononrightclick": "kpaa aka n'aka nri iji nyere aka na nkeji nke na-ahazi  isiokwu nkeji dị iche iche",
+       "tog-watchcreations": "Mgbakwụnye ihu akwụkwọ na failụ ndi a m mepụtara na ihe m ga na-elebara anya",
+       "tog-watchdefault": "Gbakwụnye ihu akwụkwọ na failụ ndi a nhaziri n'ihe m ga na-elebara anya",
+       "tog-watchmoves": "Tinye ihu akwụkwọ na failụ niile mụ bugara n'ihe m ga na-elebara anya",
+       "tog-watchdeletion": "Tinye ihu akwụkwọ na failụ niile m hichara n'ebe m ga na-elebara anya",
+       "tog-watchuploads": "Tinye failụ ohụụ m mere ọpụload n'ihe mụ ga na -elebara anya",
        "tog-minordefault": "Me ka nhoro da na orü ntakịrị níle",
        "tog-previewontop": "Zitú ntàkịrị mgbe opuzọr zi igbe orü",
        "tog-previewonfirst": "Zitú nke takírí orü mbu",
-       "tog-enotifwatchlistpages": "Türüm e-mail mgbe ihü nor na ndetu ihem ne lé gbanwere",
+       "tog-enotifwatchlistpages": "Ziterem e-mail mgbe otu ihu akwụkwọ maọbụ otu failụ nọ n'ihe ndị na-elebara anya gbanworo",
        "tog-enotifusertalkpages": "Türüm e-mail ngbe ébé okwu ndi na banife nkem gbanwere",
-       "tog-enotifminoredits": "Türüm e-mail maka orü ntakịrị ihüá",
+       "tog-enotifminoredits": "Zitekwara m email maka mgbanwo dị n'ihu akwụkwọ na failụ nhaziri",
        "tog-enotifrevealaddr": "Zifór áhàebeíbị e-mail m na e-mail okwuókà",
        "tog-shownumberswatching": "Zi onuogụgụ ndi na banife nke ne lé",
-       "tog-oldsig": "Létu ntakiri ndẹlu ejị a ma gí:",
+       "tog-oldsig": "Mbinye aka ọpụpụ gị",
        "tog-fancysig": "Mesò ka nkábi nwéré édé wiki (nké énwéghị jikodo nke nọr na onwe)",
-       "tog-uselivepreview": "Jí nlé ntàkírí dí ndụ (Í gí nwé JavaScript) (mmètú kanyí lé)",
+       "tog-uselivepreview": "Gosi  ihe ndịna na-ebupụtachaghị ihe niile dị n'ihu akwụkwọ",
        "tog-forceeditsummary": "Gwam mgbè okwu nsem màkà orüm rürü a díghị",
        "tog-watchlisthideown": "Zonari orüm fwuör ndetu ihem ne lé",
        "tog-watchlisthidebots": "Zonari orü bot fwuör ndetu ihem ne lé",
        "tog-watchlisthideminor": "Zonari orü ntakịrị fwuör ndetu ihem ne lé",
        "tog-watchlisthideliu": "Zonari orü ndi na banife nke ndi banyèrè a banyé, fwuör ndetu ihem ne lé",
+       "tog-watchlistreloadautomatically": "Gosigharia ihe nlebara anya ozigbo e nwere ihe mgbanwe (java script required)",
+       "tog-watchlistunwatchlinks": "Tinye ihe nriba ama ozigbo ozigbo({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) iji leba anya n'ihe mgbanwo dị n'ihu akwukwọ.(JavaScript required for toggle functionality)",
        "tog-watchlisthideanons": "Zonari orü ndi na banife nke ndi ámághị, fwuör ndetu ihem ne lé",
        "tog-watchlisthidepatrolled": "Zonari orü ha nè léfù ányá fwuör ndetu ihem ne lé",
+       "tog-watchlisthidecategorization": "zoo nkewasị nke ihu akwụkwọ",
        "tog-ccmeonemails": "Sipu iye e-mail m na sipu ndi ozor",
        "tog-diffonly": "É zìkwàlà ihe nọr na ihü di okpúrù íchiè",
        "tog-showhiddencats": "Zi ébéonọr zonari a zonari",
        "tog-norollbackdiff": "Kwà diff mgbe byárá na mgbe láázú mèchàrà",
+       "tog-useeditwarning": "gwam mgbe m hapụrụ ihu akwụkwọ nhaziri na echekwaghị ihe ndị m gbamworo",
        "underline-always": "M̀gbèọbụlà",
        "underline-never": "Emelaème",
        "underline-default": "Ndatụ ihü njikota",
        "september-date": "Ọnwaìtoolu $1",
        "october-date": "Ọnwaìri $1",
        "november-date": "Ọnwaìrinàotù $1",
-       "december-date": "Ọnwaìrinààbụọ",
+       "december-date": "Ọnwa Iri na abụọ $1",
        "pagecategories": "{{PLURAL:$1|Ụdàkọ}}",
        "category_header": "Ihu nà ime ụdàkọ \"$1\"",
        "subcategories": "Ụdàkọòkpurù",
        "newwindow": "(o na mepo na onyonyo ohúrù)",
        "cancel": "Hapụ̀",
        "moredotdotdot": "Ozókwá...",
+       "morenotlisted": "Ọdika ndepụta a ezugioke.",
        "mypage": "Ihü",
        "mytalk": "Nkàta",
        "anontalk": "Nkàta",
        "returnto": "Ganata na $1.",
        "tagline": "Oshị {{SITENAME}}",
        "help": "Inyeáká",
+       "help-mediawiki": "Enyemaka gbasara MidiyaWiki",
        "search": "Tùwe",
        "searchbutton": "Tùwe",
        "go": "Gá",
        "searcharticle": "Gá",
        "history": "Ịta ihüá",
        "history_short": "Ịta",
+       "history_small": "akụkọ ihe mere eme",
        "updatedmarker": "ihe gáráníru ké mgbe m byàrà nga mbu",
        "printableversion": "Ùdì ǹke mbipụ̀",
        "permalink": "Jikodo ekechịrị",
index c963d30..96fd566 100644 (file)
        "post-expand-template-argument-category": "Лерий теркал яь йоаца белгалонаш (аргументаш) чулоацаш оагӀонаш",
        "undo-failure": "Ер нийсдар юхадаккха йиш яц юкъе даь хувцамаш бахьане",
        "viewpagelogs": "Укх оагӀон тептараш хьахьокха",
+       "currentrev": "ХIанзара эрш",
        "currentrev-asof": "ТӀехьара эрш $1",
        "revisionasof": "Эрш $1",
        "revision-info": "Эрш ($1); {{GENDER:$6|$2}}$7",
        "previousrevision": "← XьалхайоагIа",
        "nextrevision": "ТIехьайоагIа →",
-       "currentrevisionlink": "ХIанзара верси",
+       "currentrevisionlink": "ХIанзара эрш",
        "cur": "карара",
        "next": "тӀехь.",
        "last": "хьалхара",
        "uploadlogpage": "Чуяьккхарий тептар",
        "filedesc": "Лоаца йоазонца сурт оттадар",
        "fileuploadsummary": "Лоаца сурт оттадар:",
+       "upload-dialog-button-upload": "Чуяккха",
        "license": "Лицензи ялар:",
        "license-header": "Лицензировани",
        "listfiles-delete": "дӀаяккха",
        "listfiles_size": "Боарам",
        "listfiles_description": "Йоазонца сурт оттадар",
        "listfiles_count": "Эрш",
+       "listfiles-latestversion": "ХIанзара эрш",
        "file-anchor-link": "Файл",
        "filehist": "Файла истори",
        "filehist-help": "Ди/ха долча IотIатоIае цу хана файл мишта хиннай хьажа йиш хуpгьйолаш",
        "sp-contributions-newbies": "Хьахьокха алхха керда дагара йоазонашца баь бола къахьегам",
        "sp-contributions-blocklog": "блок тохар",
        "sp-contributions-deleted": "{{GENDER:$1|доакъашхочун}} дӀадаьккха хинна тоадар",
-       "sp-contributions-uploads": "Ñ\87Ñ\83Ñ\8fÑ\8cÑ\85аÑ\80аш",
+       "sp-contributions-uploads": "Ñ\84айлаш",
        "sp-contributions-logs": "тептараш",
        "sp-contributions-talk": "къамаьл дар",
        "sp-contributions-userrights": "{{GENDER:$1|доакъашхочун}} бокъонашта урхалдар",
index 00f82f1..501a4b0 100644 (file)
        "returnto": "Aftur á: $1.",
        "tagline": "Úr {{SITENAME}}",
        "help": "Hjálp",
+       "help-mediawiki": "Hjálp varðandi MediaWiki",
        "search": "Leit",
        "search-ignored-headings": " #<!-- leave this line exactly as it is --> <pre>\n# Headings that will be ignored by search.\n# Changes to this take effect as soon as the page with the heading is indexed.\n# You can force page reindexing by doing a null edit.\n# The syntax is as follows:\n#   * Everything from a \"#\" character to the end of the line is a comment.\n#   * Every non-blank line is the exact title to ignore, case and everything.\nReferences\nExternal links\nSee also\n #</pre> <!-- leave this line exactly as it is -->",
        "searchbutton": "Leita",
        "badarticleerror": "Þetta er ekki hægt að framkvæma á síðunni.",
        "cannotdelete": "Ekki var hægt að eyða síðunni \"$1\".\nLíklegt er að einhver annar hafi gert það.",
        "cannotdelete-title": "Gat ekki eytt síðunni $1",
+       "delete-scheduled": "Síðan \"$1\" er áætluð til eyðingar.\nSýndu þolinmæði.",
        "delete-hook-aborted": "Eyðing síðu stöðvuð af viðbótarkrók (extension hook).\nEngin skýring gefin.",
        "no-null-revision": "Ekki var hægt að búa til nýja núll-útgáfu síðunnar \"$1\"",
        "badtitle": "Slæmur titill",
        "accmailtext": "Lykilorðið fyrir [[User talk:$1|$1]] hefur verið sent á $2. Hægt er að breyta því á síðunni ''[[Special:ChangePassword|breyta lykilorði]]'' þegar notandinn hefur skráð sig inn.",
        "newarticle": "(Ný)",
        "newarticletext": "Þú hefur fylgt tengli á síðu sem ekki er til ennþá.\nÞú getur búið til síðu með þessu nafni með því að skrifa í formið fyrir neðan\n(meiri upplýsingar í [$1 hjálpinni]).\nEf þú hefur óvart villst hingað geturðu notað '''til baka'''-hnappinn í vafranum þínum.",
-       "anontalkpagetext": "----\n<em>Þetta er spjallsíða fyrir óþekktan notanda sem hefur ekki búið til aðgang ennþá, eða notar hann ekki.</em>\nÞar af leiðandi þurfum við að nota vistfang til að bera kennsli á hann/hana.\nNokkrir notendur geta deilt sama vistfangi.\nEf þú ert óþekktur notandi og finnst að óviðkomandi athugasemdum hafa verið beint að þér, gjörðu svo vel og [[Special:CreateAccount|búðu til aðgang]] eða [[Special:UserLogin|skráðu þig inn]] til þess að koma í veg fyrir þennan rugling við aðra óþekkta notendur í framtíðinni.",
+       "anontalkpagetext": "----\n<em>Þetta er spjallsíða fyrir óþekktan notanda sem hefur ekki búið til aðgang ennþá, eða notar hann ekki.</em>\nÞar af leiðandi þurfum við að nota IP-vistfang til að bera kennsli á viðkomandi.\nSlík IP-vistföng geta nokkrir notendur deilt saman.\nEf þú ert nafnlaus notandi og finnst að óviðkomandi athugasemdum hafa verið beint að þér, gjörðu svo vel og [[Special:CreateAccount|búðu til aðgang]] eða [[Special:UserLogin|skráðu þig inn]] til þess að koma í veg fyrir þennan rugling við aðra óþekkta notendur í framtíðinni.",
        "noarticletext": "Enginn texti er á þessari síðu enn sem komið er.\nÞú getur [[Special:Search/{{PAGENAME}}|leitað að þessum titli]], í öðrum síðum,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} leitað í tengdum skrám], eða [{{fullurl:{{FULLPAGENAME}}|action=edit}} búið hana til]</span>.",
        "noarticletext-nopermission": "Það er enginn texti á þessari síðu eins og er.\nÞú getur [[Special:Search/{{PAGENAME}}|leitað að þessum titli]] í öðrum síðum, eða <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} leitað í tengdum skrám]</span>, en þú hefur ekki réttindi til þess að stofna þessa síðu.",
        "missing-revision": "Útgáfa #$1 síðunnar „{{FULLPAGENAME}}\" er ekki til.\n\nÞetta gerist oftast þegar úreld breytingaskrá tengir á síðu sem hefur verið eytt. Frekari upplýsingar eru í [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} eyðingarskránni].",
        "invalid-content-data": "Ógild efnisgögn.",
        "content-not-allowed-here": "„$1“ efni er ekki leyfilegt á síðunni [[:$2]] í hólfi \"$3\"",
        "editwarning-warning": "Ef farið er af þessari síðu gætu þær breytingar sem þú hefur gert tapast.\nEf þú ert skráður inn, þá getur þú gert þessi skilaboð óvirk í „{{int:prefs-editing}}\"-hluta kjörstillinganna þinna.",
+       "slot-name-main": "Aðal",
        "content-model-wikitext": "wiki-texti",
        "content-model-text": "hreinn texti",
        "content-model-javascript": "JavaScript",
        "right-editinterface": "Breyta notandaviðmótinu",
        "right-editusercss": "Breyta CSS-skrám annarra",
        "right-edituserjs": "Breyta JS-skrám annarra",
+       "right-editsitecss": "Breyta CSS á öllu vefsvæðinu",
+       "right-editsitejson": "Breyta JSON á öllu vefsvæðinu",
+       "right-editsitejs": "Breyta JavaScript á öllu vefsvæðinu",
        "right-editmyusercss": "Breyta þinni eigin CSS-notandaskrá",
+       "right-editmyuserjson": "Breyta þínum eigin JSON-notandaskrám",
        "right-editmyuserjs": "Breyta þinni eigin JavaScript-notandaskrá",
        "right-viewmywatchlist": "Skoða þinn eigin vaktlista",
        "right-editmywatchlist": "Breyta þínum eigin vaktlista. Athugið að nokkrar aðgerðir bæta enn við síður án þessa réttindis.",
        "ipb-sitewide": "Á öllum vefnum",
        "ipb-partial": "Að hluta",
        "ipb-pages-label": "Síður",
+       "ipb-namespaces-label": "Nafnrými",
        "badipaddress": "Ógilt vistfang",
        "blockipsuccesssub": "Bann tókst",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] hefur verið bannaður/bönnuð.<br />\nSjá [[Special:BlockList|bannaðir notendur og vistföng]] fyrir yfirlit yfir núverandi bönn.",
        "ipb-blocklist": "Sjá núverandi bönn",
        "ipb-blocklist-contribs": "Framlög fyrir {{GENDER:$1|$1}}",
        "ipb-blocklist-duration-left": "$1 eftir",
+       "block-actions": "Aðgerðir sem á að hindra:",
        "block-expiry": "Bannið rennur út:",
+       "block-options": "Viðbótar-valkostir:",
+       "block-prevent-edit": "Breytingar",
+       "block-reason": "Ástæða:",
+       "block-target": "Notandanafn eða IP-vistfang",
        "unblockip": "Afbanna notanda",
        "unblockiptext": "Endurvekja skrifréttindi bannaðra notenda eða vistfanga.",
        "ipusubmit": "Afbanna",
        "createaccountblock": "bann við stofnun nýrra aðganga",
        "emailblock": "tölvupóstur bannaður",
        "blocklist-nousertalk": "getur ekki breytt eigin spjallsíðu",
+       "blocklist-editing": "breytingar",
+       "blocklist-editing-sitewide": "breytingar (á öllum vefnum)",
+       "blocklist-editing-page": "síður",
+       "blocklist-editing-ns": "nafnrými",
        "ipblocklist-empty": "Bannlistinn er tómur.",
        "ipblocklist-no-results": "Umbeðið vistfang eða notandanafn er ekki í banni.",
        "blocklink": "banna",
        "pageinfo-display-title": "Sýnilegur titill",
        "pageinfo-default-sort": "Sjálfgefinn röðunarlykill",
        "pageinfo-length": "Lengd síðunnar (í bætum)",
+       "pageinfo-namespace": "Nafnrými",
        "pageinfo-article-id": "Einkennisnúmer síðunnar",
        "pageinfo-language": "Tungumál síðunnar",
        "pageinfo-language-change": "breyta",
        "revid": "útgáfa $1",
        "pageid": "auðkennisnúmer síðu $1",
        "gotointerwiki": "Fer af {{SITENAME}}",
+       "gotointerwiki-invalid": "Uppgefinn titill er ógildur.",
+       "gotointerwiki-external": "Þú ert í þann mund að yfirgefa {{SITENAME}} til að fara að skoða [[$2]], sem er aðskilið vefsvæði.\n\n'''[$1 Halda áfram á $1]'''",
        "pagedata-title": "Síðugögn",
        "pagedata-bad-title": "Ógildur titill: $1.",
        "passwordpolicies": "Stefna varðandi lykilorð",
index d0f6749..b5360b8 100644 (file)
                        "Sarah Bernabei",
                        "Wedhro",
                        "Malore",
-                       "Ruthven"
+                       "Ruthven",
+                       "Manfredi26"
                ]
        },
        "tog-underline": "Sottolinea i collegamenti:",
index b752fa6..7fa149e 100644 (file)
@@ -73,7 +73,8 @@
                        "Doyoon1995",
                        "Jay94ks",
                        "Ryuch",
-                       "Delim"
+                       "Delim",
+                       "Comjun04"
                ]
        },
        "tog-underline": "링크에 밑줄 긋기:",
        "special-characters-title-endash": "en 대시",
        "special-characters-title-emdash": "em 대시",
        "special-characters-title-minus": "빼기 기호",
-       "mw-widgets-abandonedit": "먼저 저장하지 않고 편집을 그만 두겠습니까?",
+       "mw-widgets-abandonedit": "정말 저장하지 않고 편집을 그만 두겠습니까?",
        "mw-widgets-abandonedit-discard": "편집 버리기",
        "mw-widgets-abandonedit-keep": "편집 계속",
        "mw-widgets-abandonedit-title": "확실합니까?",
index 2ce64b9..3bf8c6a 100644 (file)
@@ -11,7 +11,8 @@
                        "Nemo bis",
                        "Amire80",
                        "Beyronvan",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Lorestani"
                ]
        },
        "tog-underline": "هوم پئیڤأند زیرخأط دار:",
        "editfont-monospace": "فونت تأک بألگە یی",
        "editfont-sansserif": "فونت سان سئریف",
        "editfont-serif": "فونت سئريف",
-       "sunday": "یٱشمٱ",
+       "sunday": "یاٛ شٱمٱ",
        "monday": "دۏشٱمٱ",
-       "tuesday": "سئشمٱ",
+       "tuesday": "ساÙ\92 Ø´Ù±مٱ",
        "wednesday": "چارشٱمٱ",
-       "thursday": "پن شمٱ",
-       "friday": "جومٱ",
+       "thursday": "پٱÙ\86 Ø´Ù\85Ù±",
+       "friday": "جۏمٱ",
        "saturday": "شٱمٱ",
        "sun": "یٱشمٱ",
        "mon": "دۏشٱمٱ",
-       "tue": "ساÙ\9bØ´مٱ",
+       "tue": "ساÙ\92 Ø´Ù±مٱ",
        "wed": "چارشمٱ",
        "thu": "پٱن شمٱ",
        "fri": "جومە",
@@ -80,7 +81,7 @@
        "march": "مارس",
        "april": "آڤریل",
        "may_long": "ماٛی",
-       "june": "جوٱن",
+       "june": "جوئٱن",
        "july": "جۊلای",
        "august": "آگوست",
        "september": "سپتامر",
        "mar": "مارس",
        "apr": "آڤریل",
        "may": "ماٛی",
-       "jun": "جوٱن",
+       "jun": "جوئٱن",
        "jul": "جۊلای",
        "aug": "آگوست",
        "sep": "سپتامر",
        "subcategories": "زيردأسە یا",
        "category-media-header": "ڤارسگٱر د دٱسٱ \"$1\"",
        "category-empty": "ای دٱسٱ د راستٱکی د ڤٱرگرتٱ هیچ بٱلگٱ یا ڤارسگٱری نی",
-       "hidden-categories": "{{PLURAL:$1|دٱسٱ قام بیٱ|دٱسٱیا قام بیٱ}}",
+       "hidden-categories": "{{PLURAL:$1|دٱسٱ قایم بیٱ|دٱسٱیا قایم بیٱ}}",
        "hidden-category-category": "دأسە یا قام بییە",
-       "category-subcat-count": "{{PLURAL:$2|ای دٱسٱ فٱقٱت زیر دٱسٱ یا دئماگر هان دش.ای دٱسٱ {{PLURAL:$1|زیردٱسٱ|$1 زیردٱسٱیا}} ٱ , ڤ دٱر د $2 کول.}}",
+       "category-subcat-count": "{{PLURAL:$2|اؽ دٱسٱ فقٱت زؽر دٱسٱیا دماگر هان دش.اؽ دٱسٱ {{PLURAL:$1|زؽردٱسٱ|$1 زؽردٱسٱیا}} ٱ , ڤ دٱر د $2 کولٛ.}}",
        "category-subcat-count-limited": "ئی دأسە ها د {{PLURAL:$1|زیردأسە|$1 زیردأسە یا}} یی کئ ها ڤئ دومئشوٙ",
-       "category-article-count": "{{PLURAL:$2|ای دٱسٱ د ڤٱرگرتٱ بٱلگٱ نئهاییٱ.| {{PLURAL:$1| بٱلگٱ هی|$1 بٱلگٱیا هین}} د ای دٱسٱ، ڤ دٱر د $2 کول.}}",
+       "category-article-count": "{{PLURAL:$2|اؽ دٱسٱ د ڤٱرگرتٱ بٱلگٱ نهاییٱ.| {{PLURAL:$1| بٱلگٱ هؽ|$1 بٱلگٱیا هؽسن}} د اؽ دٱسٱ، ڤ دٱر د $2 کولٛ.}}",
        "category-article-count-limited": "نئها {{PLURAL:$1|بألگە هی|$1بألگە یا هئن}} د دأسە ئیسئنی.",
        "category-file-count": "{{PLURAL:$2|ای دٱسٱ فٱقٱت د ڤٱرگرتٱ جانیا نئهاییٱ.| نئهایی {{PLURAL:$1|جانیا هی|$1 جانیایا هین}} د ای دٱسٱ، ڤ دٱر د کول $2 .}}",
        "category-file-count-limited": " {{PLURAL:$1|[جانیا هی|1$جانیایا هین}} نئهایی هان د دأسە ئیسئنی.",
        "mytalk": "چٱک چنٱ",
        "anontalk": "دئبارە تیرنئشوٙن ئی آی پی قئسە بأکیت",
        "navigation": "ناڤجۊری",
-       "and": "&#32;و",
+       "and": "&#32;ۉ",
        "faq": "ئف ئی کیوٙ",
        "actions": "کونئشتکاریا",
        "namespaces": "نوم ڤارگٱیا",
-       "variants": "آلشتگریا",
+       "variants": "آلشتگریٛا",
        "navigation-heading": "نوم جاگٱ ناڤگردی",
        "errorpagetitle": "غألأط",
        "returnto": "ڤورگأشتئن د $1.",
        "searchbutton": "پاٛ جۊری",
        "go": "رو",
        "searcharticle": "رۉ",
-       "history": "ڤیرگار بألگە",
+       "history": "ڤیرگار بٱلگٱ",
        "history_short": "ڤیرگار",
        "updatedmarker": "د آخئری دییئن مئنە ڤئ هنگوم کو",
        "printableversion": "نۏسخٱ پلا بینی",
        "permalink": "هوم پاٛڤٱن هٱمیشاٛیی",
        "print": "چاپ گئرئتئن",
-       "view": "دیاٛن",
+       "view": "دیین",
        "view-foreign": "د $1 نه بوینیت",
-       "edit": "ڤیرایشت",
+       "edit": "ڤیرایش",
        "edit-local": "توضی ڤولات نئشینی نە ڤیرایئشت بأکیت",
        "create": "راس كئردئن",
        "create-local": "بئ گئل توضی ڤولات نئشینی ئضاف بأکیت",
        "newpage": "بألگە نۊ",
        "talkpagelinktext": "چٱک چنٱ",
        "specialpage": "بألگە ڤیجە",
-       "personaltools": "ٱڤزاریا شٱخسی",
+       "personaltools": "ٱڤزارؽا شٱخسی",
        "talk": "گٱپ",
-       "views": "دیئن",
-       "toolbox": "ٱڤزاریا",
+       "views": "دیین",
+       "toolbox": "ٱڤزارؽا",
        "imagepage": "ديئن بألگە جانیا",
        "mediawikipage": "ديئن بألگە پئيغوم",
        "templatepage": "دیئن بٱلگٱ چۊٱ",
        "viewhelppage": "ديئن بألگە هومیاری",
        "categorypage": "ديئن بألگە دأسە بأنی",
        "viewtalkpage": "دیئن چأک چئنە یا",
-       "otherlanguages": "د زڤونیا هٱنی",
-       "redirectedfrom": "(ڤاگأردوٙنی د$1)",
+       "otherlanguages": "ڤ زڤونیٛا هنی",
+       "redirectedfrom": "(ڤاگٱردونی د$1)",
        "redirectpagesub": "بألگە ڤاگأردوٙنی",
        "redirectto": "ڤاگأردوٙنی سی:",
-       "lastmodifiedat": "ای بلگٱ ایسنیا آلشت بیٱ د $1، د $2.",
+       "lastmodifiedat": "اؽ بٱلگٱ ایسنؽا آلشت بیٱ د $1، د $2.",
        "viewcount": "ئی بألگە ها د دأسرئسی {{PLURAL:$1|یئ گئل|$1 چأن گئل}}.",
        "protectedpage": "بألگە پأر و پیم کاری بییە",
-       "jumpto": "پراÙ\9bستن د:",
+       "jumpto": "پرسن د:",
        "jumptonavigation": "ناڤجۊری",
        "jumptosearch": "پاٛ جۊری",
        "view-pool-error": "د بأدبأختی،ئیسئ رئسینە جایا فئرە شولوٙغە.\nکاریاریا فئرە یی میهان ئی بألگە نە سئیل بأکأن.\nدئما یە کئ میهایت د نۊ ئی بألگە نە سئیل بأکیت یئ گوری آهئرە داری بأکیت.",
        "aboutpage": "Project:دبارٱ",
        "copyright": "مینۊنٱیا هان د دٱسرس $1 مٱر یٱ کاٛ ڤ یاٛ گاٛل شیڤاٛ هٱنی نیسٱنٱ بۊٱ.",
        "copyrightpage": "{{ns:project}}:کوپی رایت",
-       "currentevents": "روخ ڤنیا ایساٛنی",
-       "currentevents-url": "Project:رÙ\88Ø® Ú¤Ù±Ù\86Û\8cا Ø§Û\8cساÙ\9bنی",
-       "disclaimers": "تیٱپۊشکاریا",
-       "disclaimerpage": "پوروجٱ: تیٱپوشی کردن همٱگیر",
+       "currentevents": "روخ ڤنؽا ایسنی",
+       "currentevents-url": "Project:رÙ\88Ø® Ú¤Ù\86ؽا Ø§Û\8cسنی",
+       "disclaimers": "تیٱپۊشکاریٛا",
+       "disclaimerpage": "پرۉژٱ: تیٱپوشی کردن همٱگیر",
        "edithelp": "هومياری سی ڤیرایشت",
        "helppage-top-gethelp": "هومياری",
-       "mainpage": "سرآسۊنٱ",
+       "mainpage": "سرآسونٱ",
        "mainpage-description": "سرآسونٱ",
        "policy-url": "پوروجە:رأدیارجوٙری",
-       "portal": "درآسۊنٱ کومولٱیکی",
-       "portal-url": "پوروجٱ:درآسۊنٱ کومولٱیکی",
+       "portal": "درآسونٱ کومولٱیکی",
+       "portal-url": "پرۉژٱ:درآسونٱ کومولٱیکی",
        "privacy": "پۊلتیک رازینٱداری",
-       "privacypage": "پوروجٱ: راگوڤاری رازینٱ کاری کردن",
+       "privacypage": "پرۉژٱ: راگوڤاری رازینٱ کاری کردن",
        "badaccess": "خأطا صئلا دأئن",
        "badaccess-group0": "شوما صئلا أنجوم دأئن کاری نە کئ میهایت ناریت.",
        "badaccess-groups": "ئی کاری کئ شوما هاستیتە سی کاریاریایی کئ هان د {{PLURAL:$2|جأرغە|یئ گئل د جأرغە یا}}: $1 کأم بییە.",
        "newmessageslinkplural": "{{PLURAL:$1|یئ گئل پئیغوم تازە|999=پئیغومیا تازە}}",
        "newmessagesdifflinkplural": "آخئر {{PLURAL:$1|آلئشت|آلئشتیا}}",
        "youhavenewmessagesmulti": "شوما یئ گئل پئیغوم تازە د $1 داریت",
-       "editsection": "ڤیرایشت",
+       "editsection": "آلشدکاری",
        "editold": "ڤیرایئشت",
        "viewsourceold": "سئیل د سأرچئشمە بأکیت",
-       "editlink": "ڤیرایشت",
-       "viewsourcelink": "ساٛیل د سرچشمٱ بٱکیت",
-       "editsectionhint": "ڤیرایشت یاٛ گاٛل بٱرجا:$1",
-       "toc": "مینۊنٱیا",
+       "editlink": "ڤیرایش",
+       "viewsourcelink": "ساٛلٛ د سرچشمٱ بٱکؽت",
+       "editsectionhint": "ڤیرایش یاٛ بٱرجا:$1",
+       "toc": "مؽنونٱیا",
        "showtoc": "نئشوٙ دأئن",
        "hidetoc": "قام کئردئن",
        "collapsible-collapse": "جأم کئردئن",
        "site-rss-feed": "هأڤال حوٙن RSS سی $1",
        "site-atom-feed": "هوڤال هۊ Atom سی $1",
        "page-rss-feed": "هأڤال حوٙن RSS سی «$1»",
-       "page-atom-feed": "هأڤال حوٙن Atom سی $1",
+       "page-atom-feed": "هٱڤال هۊن Atom سی $1",
        "feed-atom": "اتم",
        "feed-rss": "آر اس اس",
-       "red-link-title": "$1(بÙ\84Ú¯Ù± Ù\86Û\8cاÙ\9bش)",
+       "red-link-title": "$1(بٱÙ\84Ú¯Ù± Ù\86ؽسش)",
        "sort-descending": "کأم بییئن سأرجاخود",
        "sort-ascending": "زياد بيیئن سأرجادخود",
        "nstab-main": "بٱلگٱ",
        "nstab-user": "بلگٱ کاریار",
        "nstab-media": "بألگە ڤارئسگأر",
-       "nstab-special": "بÙ\84Ú¯Ù±Û\8cا Û\8bÛ\8cجٱ",
+       "nstab-special": "بٱÙ\84Ú¯Ù±Û\8cا Ú¤Û\8cÚ\98ٱ",
        "nstab-project": "بألگە پوروجە",
-       "nstab-image": "جانیا",
+       "nstab-image": "جانؽا",
        "nstab-mediawiki": "پئیغوٙم",
        "nstab-template": "چۊٱ",
        "nstab-help": "بألگە هومیاری",
        "nstab-category": "دٱسٱ",
-       "mainpage-nstab": "سرآسۊنٱ",
+       "mainpage-nstab": "سرآسونٱ",
        "nosuchaction": "چئنی کونئشتگأری نییئش",
        "nosuchactiontext": "کاری کئ ڤا یوٙ آر ئل تیار بییە نادیارە.\nگاسی شوما یوٙ آر ئل نە دوروس نأنیسأنیتە، یا یئ گئل هوم پئیڤأند ئشتئڤا ڤارئد بییە.\nڤئ گاسی یئ گئل سیسئریک د نأرم أفزاز ڤئ کار گئرئتە بییە ڤا {{SITENAME}} ئشارە بأکە.",
        "nosuchspecialpage": "چئنی بألگە ڤیجە یی نییئش",
        "createacct-another-username-ph": "نوم کاریاری توٙنە بأزئنیت",
        "yourpassword": "رازینە گوڤاردئن:",
        "userlogin-yourpassword": "رازینە گوڤاردئن",
-       "userlogin-yourpassword-ph": "رازینٱ گوڤارسناْ بٱزاْ",
+       "userlogin-yourpassword-ph": "رازینٱ گوئارسناْ بٱزاْ",
        "createacct-yourpassword-ph": "رازینە گوڤاردئن نە بأزە",
        "yourpasswordagain": "یئ گئل هأنی رازینە گوڤاردئن نە بأزە",
        "createacct-yourpasswordagain": "رازینە گوڤاردئن نە پوشت راس کو",
        "userlogin-noaccount": "یئ گئل حئساڤ ناریت؟",
        "userlogin-joinproject": "أندوم دیارگە {{SITENAME}} بوٙئیت",
        "createaccount": "حئساڤ راس بأکیت",
-       "userlogin-resetpassword-link": "رازینٱ گوڤارسنتو د ڤیرتو رٱتٱ؟",
+       "userlogin-resetpassword-link": "رازینٱ گوئارسن تو د ڤیرتو رٱتٱ؟",
        "userlogin-helplink2": "هومیاری کئردئن د طأریق ڤامین ئوٙمائن",
        "userlogin-loggedin": "شوما ئیسئ چی یئ گئل {{GENDER:$1|$1}} ئوٙمایتە ڤامین.نوم بألگە هاری نە سی ڤامین ئوٙمائن چی یئ گئل کاریار هأنی بلگه هاری سی وا مین اومائن چی یه گل کاریار هنی ڤئ کار بئیریت.",
        "userlogin-createanother": "یئ گئل حئساڤ هأنی راس بأکیت",
        "loginlanguagelabel": "زۊن:$1",
        "suspicious-userlogout": "د حاست ڤئ دأر رأتئن شوما تیە پوشی بییە سی یە کئ ڤئ نأظأر یما کئ ڤئ سی یئ گئل دوڤارتە نیأر گأن یا یئ گئل پوروکسی کئ ها د ڤیرگە کأش کئل بییە.",
        "createacct-another-realname-tip": "نوم راستأکی دئل ڤئ حاییە.\nأر شوما ڤئنە نئها ئمایە بأکیت، یە سی هوم نئسبأت دأئن کاریاری سی کاریاش ڤئ کار گئرئتئ بوٙە.",
-       "pt-login": "ڤا مؽݩ اومایݩ",
+       "pt-login": "ڤا مؽن اوماین",
        "pt-login-button": "ڤامین ئوٙمائن",
-       "pt-createaccount": "Ù\87ساÙ\88 Ø±Ø§س بٱکؽت",
+       "pt-createaccount": "Ù\87ساÙ\88 Ø¯Û\8fرس بٱکؽت",
        "pt-userlogout": "د سامۊنٱ دراۊمائن",
        "php-mail-error-unknown": "خأطا نادیار د آلئشتگئر PHP's mail()",
        "user-mail-no-addy": "سی کئل کئردئن أنجومانامە د یئ گئل أنجومانامە بی تیرنئشوٙن أنجومانامه تئلاش بییە.",
        "subject-preview": "داسوٙن/پیش سئیل سأرخأط:",
        "previewerrortext": "یئ گئل خأطا د گاتی کئ شوما میهاستیت یئ گئل پیش سئل د آلئشتیاتوٙ داشتوٙین پیش ئوٙماە.",
        "blockedtitle": "کاریار نئهاگئری بی",
-       "blockedtext": "<strong>Ù\86Ù\88Ù\85 Ú©Ø§Ø±Û\8cارÛ\8c Ø´Ù\88Ù\85ا Û\8cا ØªÛ\8cرÙ\86Ø´Û\8aÙ\86 Ø¢Û\8c Ù¾Û\8c Ø´Ù\88Ù\85ا Ù\86اÙ\9bÙ\87اگرÛ\8c Ø¨Û\8cÙ±.</strong>\n\n\n$1 Ú¤Ù\86Ù± Ù\86اÙ\9bÙ\87اگرÛ\8c Ú©Ø±Ø¯Ù±.\nدÙ\84Û\8cÙ\84Ø´ Ù\87ا Ø¯ Ø§Û\8cÚ\86اÙ\9b<em>$2</em>.\n\nØ´Ù\88رÛ\8a Ø¯ Ù\86اÙ\9bÙ\87اگرÛ\8c:$8\nآخر Ù\86اÙ\9bÙ\87اگرÛ\8c:$6\nکارÛ\8cارÛ\8c Ú©Ø§Ù\9b Ù\87ا Ú¤Ù±Ø±ØªÛ\8cÙ± Ù\88 Ù\86اÙ\9bÙ\87اگرÛ\8c Ø¨Û\8cÙ±:$7\n\nØ´Ù\88Ù\85ا Ù\85Û\8c ØªÛ\8aÙ\86Û\8cت Ú¤Ø§ $1 Û\8cا [[{{MediaWiki:Grouppage-sysop}}|سردÛ\8cÚ¤Û\8aÙ\86کار]] Ù\87Ù±Ù\86Û\8c Ù¾Û\8cÚ¤Ù\86د Ø¨Ø§Ù\9bÛ\8cرÛ\8cت Ù\88 Ø³Û\8c Ù\86اÙ\9bÙ\87اگرÛ\8c Ú¤Ø§ Ú¤ Ú\86Ù±Ú© Ú\86Ù\86Ù± Ú©Ø§Ø±Û\8c Ø¨Ù±Ú©Û\8cت.\nÙ±Ù\84ڤت Ø¯ Ú¤Û\8cر Ø¯Ø§Ø´ØªÛ\8aئÛ\8cت Ú©Ø§Ù\9b Ø´Ù\88Ù\85ا Ù\86Ù\85Û\8c ØªÛ\8aÙ\86Û\8cت Ú\86Û\8cا Ù±Ù\86جÙ\88Ù\85اÙ\86اÙ\85Ù± Ú©Ø§Ù\9bÙ\84 Ú©Ø±Ø¯Ù\86 Ø³Û\8c Ø§Û\8c Ú©Ø§Ø±Û\8cار Ù\86Ù± Ú¤ Ú©Ø§Ø± Ø¨Ø§Ù\9bÛ\8cرÛ\8cتØ\8c Ù\85ٱر Û\8cÙ± Ú©Ø§Ù\9b Û\8cاÙ\9b Ú¯Ù±Ù\84 ØªÛ\8cرÙ\86Ø´Û\8aÙ\86 Ù±Ù\86جÙ\88Ù\85اÙ\86اÙ\85Ù± Ù\86ازار Ù\86Ù± [[Special:Preferences|تٱرجÛ\8cئات Ú©Ø§Ø±Û\8cارÛ\8c]] Ø®Ù\88تÛ\8a Ø¯Û\8cارÛ\8c Ú©Ø±Ø¯Û\8aئÛ\8cت Ù\88 ØªÛ\8aÙ\86ستÛ\8aÛ\8cت Ú¤ Ù\86Ù± Ú¤ Ú©Ø§Ø± Ø¨Ø§Ù\9bÛ\8cرÛ\8cت .\nتÛ\8cرÙ\86Ø´Û\8aÙ\86 Ø¢Û\8c Ù¾Û\8c Ø§Û\8cساÙ\9bÙ\86Û\8c Ø´Ù\88Ù\85ا $3 Ù±Ø\8c Ù\88 Ù\86Ù\88Ù\85 Ø¯Û\8cارکÙ\88Ù\86 Ù\86اÙ\9bÙ\87اگرÛ\8c #$5 Ù±.\nÙ\84Ù\88تÙ\81 Ø¨Ù±Ú©Û\8cت Ù\87Ù\85Ù± Ú\86Û\8cاÙ\86Ù± Ø¯ Ù\87ر Ù\87استٱÛ\8cÛ\8c Ú©Ø§Ù\9b Ø¯Ø§Ø±Û\8cت Ø¨Û\8aئÛ\8cت.",
+       "blockedtext": "<strong>Ù\86Ù\88Ù\85 Ú©Ø§Ø±Û\8cارÛ\8c Ø´Ù\85ا Û\8cا ØªÛ\8cرÙ\86Ø´Ù\88Ý© Ø¢Û\8c Ù¾Û\8c Ø´Ù\85ا Ù\86Ù\87اگرÛ\8c Ø¨Û\8cÙ±.</strong>\n\n\n$1 Ú¤Ù\86Ù± Ù\86Ù\87اگرÛ\8c Ú©Ø±Ø¯Ù±.\nدÙ\84Ù\9bÛ\8cÙ\84Ù\9bØ´ Ù\87ا Ø¯ Ø§Û\8cÚ\86اÙ\92<em>$2</em>.\n\nشرÛ\8a Ø¯ Ù\86Ù\87اگرÛ\8c:$8\nآخر Ù\86Ù\87اگرÛ\8c:$6\nکارÛ\8cارÛ\8c Ú©Ø§Ù\92 Ù\87ا Ú¤Ù±Ø±ØªÛ\8cÙ± Û\89 Ù\86Ù\87اگرÛ\8c Ø¨Û\8cÙ±:$7\n\nØ´Ù\85ا Ù\85ؽ ØªÙ\88Ù\86ؽت Ú¤Ø§ $1 Û\8cا [[{{MediaWiki:Grouppage-sysop}}|سردÛ\8cÚ¤Ù\88Ù\86کار]] Ù\87Ù\86Û\8c Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 Ø¨Ø§Ù\9bÛ\8cرؽت Û\89 Ø³Û\8c Ù\86Ù\87اگرÛ\8c Ú¤Ø§ Ú¤Ù± Ú\86Ù±Ú© Ú\86Ù\86Ù± Ú©Ø§Ø±Û\8c Ø¨Ù±Ú©Ø½Øª.\nÙ±Ù\84ڤٱت Ø¯ Ú¤Û\8cر Ø¯Ø§Ø´ØªÛ\8aؽت Ú©Ø§Ù\92 Ø´Ù\85ا Ù\86Ù\85ؽ ØªÙ\88Ù\86ؽت Ú\86Û\8cا Ù±Ù\86جÙ\88Ù\85 Ù\86اÙ\85Ù± Ú©Ù\84Ù\9b Ú©Ø±Ø¯Ù\86 Ø³Û\8c Ø§Ø½ Ú©Ø§Ø±Û\8cار Ù\86اÙ\92 Ú¤ Ú©Ø§Ø± Ø¨Ø§Ù\9bÛ\8cرؽتØ\8c Ù\85ٱر Û\8cÙ± Ú©Ø§Ù\92 Û\8cاÙ\9b ØªÛ\8cرÙ\86Ø´Ù\88Ý© Ù±Ù\86جÙ\88Ù\85 Ù\86اÙ\85Ù± Ù\86ازار Ù\86Ù± [[Special:Preferences|تٱرجÛ\8cÛ\8cات Ú©Ø§Ø±Û\8cارÛ\8c]] Ø®Ù\88تÙ\88 Ø¯Ø½Ø§Ø±Û\8c Ú©Ø±Ø¯Û\8aؽت Û\89 ØªÙ\88Ù\86سÛ\8aؽت Ú¤Ù±Ù\86اÙ\92 Ú¤ Ú©Ø§Ø± Ø¨Ø§Ù\9bÛ\8cرؽت .\nتÛ\8cرÙ\86Ø´Ù\88Ý© Ø¢Û\8c Ù¾Û\8c Ø§Û\8cسÙ\86Û\8c Ø´Ù\85ا $3 Ù±Ø\8c Û\89 Ù\86Ù\88Ù\85 Ø¯Ø½Ø§Ø±Ú©Ù\88Ý© Ù\86Ù\87اگرÛ\8c #$5 Ù±.\nÙ\84Ù\88تÙ\81 Ø¨Ù±Ú©Ø½Øª Ù\87Ù\85Ù± Ú\86Û\8cاÙ\86اÙ\92 Ø¯ Ù\87ٱر Ù\87استاÙ\9bÛ\8cؽ Ú©Ø§Ù\92 Ø¯Ø§Ø±Ø½Øª Ø¨Û\89ؽت.",
        "autoblockedtext": "<strong>نوم کاریاری شوما یا تیرنئشوٙن آی پی شوما سی یە کئ یئ گئل کاریاری هأنی ڤئ نە ڤئ کار گئرئتە خودأنجومأن ڤئ دأس $1 نئهاگئری بییە.</strong>\n\n\n$1 ڤئنە نئهاگئری کئردە.\nدألیلئش ها د ئیچئ\n<em>$2</em>.\n\nشوروٙ د نئهاگئری:$8\nآخئر نئهاگئری:$6\nکاریاری کئ ها ڤأرتیە و نئهاگئری بییە:$7\n\nشوما می توٙنیت ڤا $1 یا [[{{MediaWiki:Grouppage-sysop}}|سأردیڤوٙنکار]] هأنی پئیڤأند بئیریت و سی نئهاگئری ڤا ڤئ چأک چئنە کاری بأکیت.\nألڤأت د ڤیر داشتوٙئیت کئ شوما نئمی توٙنیت خوصۉصیأت أنجومانامە کئل کئردئن سی ئی کاریار نە ڤئ کار بئیریت، مأر یە کئ یئ گئل تیرنئشوٙن أنجومانامە نازار نە [[Special:Preferences|تأرجیحات کاریاری]] خوتوٙ دیاری کئردئوٙیت و توٙنئستوٙیت ڤئ نە ڤئ کار بئیریت .\nتیرنئشوٙن آی پی ئیسئنی شوما $3ە، و نوم دیارکون نئهاگئری #$5 ە.\nلوطف بأکیت هأمە چیانە د هأر حاستە یی کئ داریت بوٙئیت.",
        "blockednoreason": "هیچ دألیلی گوتە نأبییە",
        "whitelistedittext": "$1 لوطف بأکیت بألگە یا نە ڤیرایئشت کاری بأکیت.",
        "newarticle": "تازە",
        "newarticletext": "شوما هایین ڤا دئما هوم پئیڤأندی کئ ڤوجوٙد نارە.\nسی رأڤأندیاری بألگە.شوروٙ بأکیت مینئ جأڤە هاری بأنیسیت (سی دوٙنئسئن بیشتئر سئیل [$1 ] بأکیت).\nأر شوما سی ئشتئڤا کئردئن هائیت ئیچئ، ری دوگمە ڤادئما رأتئن دوڤارتە نیأر بأپوٙرنیت.",
        "anontalkpagetext": "----",
-       "noarticletext": "د ایساٛنیا ای بلگٱ نیسسٱ ڤوجۊد ناشتٱ.\nشوما می تۊنیت د[[Special:Search/{{PAGENAME}}|بگردید]] د ای بلگٱ ای د بلگٱ هٱنی یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د نۏ پی جۊری بۊٱ]</span>، <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} یا ای بلگٱ ناٛ ڤیرایشت بٱکیت]</span>.",
-       "noarticletext-nopermission": "د ایساٛنیا ای بلگٱ نیسسٱ یی ۋوجۊد ناشتٱ.\nشوما می تۊنیت د[[Special:Search/{{PAGENAME}}|بگردید]] د ای بلگٱ یا د بلگٱ هٱنی یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د نۏ پی جۊری بۊٱ]</span>، <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}}</span>.ۋلی شوما ساٛلا یٱ نٱ کاٛ ای  بلگٱ ناٛ راس بٱکیت ناریت.",
+       "noarticletext": "د ایسنؽا اؽ بٱلگٱ نیسسٱ ڤجۊد ناشتٱ.\nشما مؽ تونؽت د[[Special:Search/{{PAGENAME}}|بٱگٱردؽد]] د اؽ بٱلگٱ اؽ د بٱلگٱ هنی یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د نۊ پاٛجۊری بۊئٱ]</span>، <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}} یا ای بٱلگٱ ناْ ڤیرایش بٱکؽت]</span>.",
+       "noarticletext-nopermission": "د ایسنؽا اؽ بٱلگٱ نیسساٛیؽ ڤجۊد ناشتٱ.\nشما مؽ تونؽت د[[Special:Search/{{PAGENAME}}|بٱگردؽد]] د اؽ بٱلگٱیا د بٱلگٱ هنی یا <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} د نۊ پاٛجۊری بۊئٱ]</span>، <span class=\"plainlinks\">[{{fullurl:{{FULLPAGENAME}}|action=edit}}</span>.ڤلی شما سلا یٱناْ کاْ اؽ بٱلگٱ ناْ دۏرس بٱکؽت نارؽت.",
        "missing-revision": "ڤانئیأری #$1 د بألگە یی کئ نومئش ڤئنە \"{{FULLPAGENAME}}\" ڤوجوٙد نارە.\n\nگاسی سی یئ گئل ڤیرگار ڤئ هئنگوم نأبییە کئ د یئ گئل بألگە پاکسا بییە هوم پئیڤأند بییە رأڤأندیاری بییە.\nگاسی جوزئیات د[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log] دیاری بأکأن.",
        "userpage-userdoesnotexist": "حئساڤ کاریاری \"$1\" ثأڤت نأم نأبییە.\nأر میھایت ئی بألگە نئ بأسازیت یا ڤیرایئشت کاری بأکیت یئ گئل ڤارئسی أنجوم بئیتوٙ.",
        "userpage-userdoesnotexist-view": "هساو کاریاری \"$1\" سٱبت ناٛییٱ.",
        "blocked-notice-logextract": "ئی کاریار د ئیسئنی نئھاگئری بییە.\nآخئری پئرئستنوٙمە نئھاگئری ڤامین یاریا د ھار سی سأرچئشمە آمادە کاری بییە:",
-       "clearyourcache": "<strong>د ڤیر داشتوٙیت:</strong> نئها ئمایە کاری،گاسی شوما مأژبوٙر د ڤئ کار ڤأنئن ڤیرگە قام بییە دوڤارتە نییأر خوتوٙ سی دیئن ئی آلئشتکاری بوٙئیت.\n* <strong>فایئرفاکس/ سأفأری:</strong> بأپوٙرنیت ری<em>شیفت</em> ئوٙسئ کئ می پوٙرنیت <em> ڤئ د نۊ سۊڤار موٙە </em>یا ھأنی ری <em>Ctrl-F5</em> بأپوٙرنیت یا<em>Ctrl-R</em> (<em>⌘-R</em> د ساموٙنە مأک)\n* <strong>گوٙگئل کئروم:</ strong>بأپوٙرنیت ری <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> یا د ساموٙنە)\n* <strong>ئینتئرنئت ئکسپئلورئر:</strong> <em>Ctrl</em> نە ڤاداریت ئوٙسئ کئ می پوٙرنیت <em>ری کئلیت رئفرئش</em>،یا ڤاداشتیتە<em>Ctrl-F5</em>\n* <strong>اوپرا:</strong> ڤیرگە قام بییە د <em>أڤزاریا→ ئولأڤی یأتیانە پاکسا بأکیت</em>",
+       "clearyourcache": "<strong>ڤ ڤیر داشتۊؽت:</strong> نوئا آمایٱ کاری،گاسؽ شما مٱجبۊر د ڤ کار ڤٱنن ڤیرگٱ قایم بیٱ دوئارتٱ نییٱر خوتوݩ سی دیین اؽ آلشتکاری بۊیؽت.\n* <strong>فایرفاکس/ سٱفٱری:</strong> بٱپۊرنؽت ری<em>شیفت</em> اۊساْ کاْ ؽ پۊرنؽت <em> ڤٱ د نۊ سڤار مۊئٱ </em>یا ھنی ری <em>Ctrl-F5</em> بٱپۊرنؽت یا<em>Ctrl-R</em> (<em>⌘-R</em> د ساموٙنە مأک)\n* <strong>گۊگل کروم:</ strong>بٱپۊرنؽت ری <em>Ctrl-Shift-R</em> (<em>⌘-Shift-R</em> یا د سامونٱ)\n* <strong>اینترنت اْکسپلورر:</strong> <em>Ctrl</em> ناْ ڤادارؽت اۊساْ کاْ مؽپۊنؽت <em>ری کلٛیلٛ رفرش</em>،یا ڤاداشتیتٱ<em>Ctrl-F5</em>\n* <strong>اوپرا:</strong> ڤیرگٱ قایم بییٱ د <em>أڤزارؽا→ آولٱڤؽٱتؽاناْ پاکسا بٱکؽتت</em>",
        "usercssyoucanpreview": "<strong>چی ڤأرتیە گئر:</strong>  دوگمە\"{{int:showpreview}}\" سی ئزماشت کاری سی ئس ئس تازە دئما ئمایە کاری ڤئ کار بئیریت.",
        "userjsyoucanpreview": "<strong>چی ڤأرتیە گئر:</strong>  دوگمە\"{{int:showpreview}}\" سی ئزماشت کاری جاڤا ئسکئریپت تازە دئما ئمایە کاری ڤئ کار بئیریت.",
        "usercsspreview": "<strong>د ڤیر داشتوٙئیت کئ شوما فأقأط می توٙنیت سی ئس ئس کاریاری توٙنە پیش سئیل بأکیت. ڤئ ئیسئنی ئمایە نأبییە!</strong>",
        "semiprotectedpagewarning": "<strong>د ڤیر داشتوٙئیت:</strong> ئی بألگە سی یە کئ فأقأط کاریاریا ثأڤتئنام کئردە تونئسوٙئن دئش ڤیراشتکار بأکأن پأر و پیم بییە.\nآخئری پئھرئستنوٙمە دأئە بییە سی سأرچئشمە ھاری نئھا ئمایە بییە:",
        "cascadeprotectedwarning": "<strong>زئنار:</strong> ئی بألگە ڤئ دأس کاریاریایی کئ صئلا سأردیڤوٙنکاری دارن می توٙنە ڤیرایئشت کاری بوٙە سی یە کئ ئی بألگە ڤئ رأڤئشت تاف نئمایی پأر و پیم کاری بییە {{PLURAL:$1|بألگە|بألگە یا}}:",
        "titleprotectedwarning": "<strong>زئنار:ئی بألگە پأر و پیم بییە سی یە کئ [[Special:نومگە حوقوٙق کاریاری جأرغە|حوقوٙق ڤیجە]] بایأد ڤئنە رأڤأندیاری بأکأن.</strong>\nآخئری پئھرئستنوٙمە دأ بییە سی سأرچئشمە دأئن نئھا ئمایە بییە:",
-       "templatesused": "{{PLURAL:$1|چۊٱ|چۊٱ یا}} ڤ کار گرتاْ بیٱ د ای بٱلگٱ:",
-       "templatesusedpreview": "{{PLURAL:$1|چۊٱ|چۊٱ یا}} ڤ کار گرتاْ بیٱ د پیش سئیل:",
+       "templatesused": "{{PLURAL:$1|چۊٱ|چۊٱ یا}} ڤ کار گرتٱ بیٱ د اؽ بٱلگٱ:",
+       "templatesusedpreview": "{{PLURAL:$1| چۊٱ|چۊٱ یا}} ڤ کار گرتٱ بیٱ د پیش ساٛلٛ :",
        "templatesusedsection": "{{PLURAL:$1|چوٙأ|چوٙأ یا}} ڤئ کار گئرئتە بییە د ئی بأرجا:",
-       "template-protected": "(پٱر و پیم بیٱ)",
+       "template-protected": "(پٱر ۉ پیم بیٱ)",
        "template-semiprotected": "(نئصم و نیمە پأر و پیم بییە)",
        "hiddencategories": "اؽ بٱلگٱ یٱکؽ د ٱندومیائٱ {{PLURAL:$1|1 hidden category|$1 hidden categories}} :",
        "edittools-upload": "-",
        "nohistory": "هیچ ویرگار ویرایشتی د ای بلگه نئ.",
        "currentrev": "آخرین دوواره دیئن",
        "currentrev-asof": "آخري وانئری چی $1",
-       "revisionasof": "دوڤارٱ دیاٛن $1",
+       "revisionasof": "دوڤارٱ دیین $1",
        "revision-info": "دوواره سیل بیه چی $1 وا $2",
-       "previousrevision": "ڤاÙ\86Û\8cٱرÛ\8c Ø¯Ø§Ù\9bÙ\85اÛ\8cÛ\8c←",
+       "previousrevision": "ڤاÙ\86Û\8cٱرÛ\8c Ø²Û\8cترÛ\8c ←",
        "nextrevision": "ڤانیٱری تازٱتر",
        "currentrevisionlink": "آخری ڤانیٱری",
        "cur": "تازٱ باۋ",
        "difference-title-multipage": "فرخ مینجا بلگه یا \"$1\" و \"$2\"",
        "difference-multipage": "(فرخ مینجا بلگه یا)",
        "lineno": "خٱت $1:",
-       "compareselectedversions": "دÙ\88ارٱ Ø¯Û\8cÙ\86Û\8cاؽؽ Ú©Ø§Ù\92 Ø§Ù\92Ù\86تخاÙ\88 Ø¨Û\8cÙ\86Ù± Ù\86اÙ\92 Ù\85Ù\88Ù\82اÛ\8cسٱ Ø¨Ù±Ú©Ø½ØªÙ\88",
+       "compareselectedversions": "دÙ\88ئارٱ Ø¯Û\8c Û\8cÙ\86ؽاÛ\8cؽ Ú©Ø§Ù\92 Ø§Ù\92Ù\86تخاÙ\88 Ø¨Û\8cÙ\86Ù± Ù\86اÙ\92 Ù\85Ù\88Ù\82اÛ\8cسٱ Ø¨Ù±Ú©Ø½Øª",
        "showhideselectedversions": "شلک دیئن وانیریا انتخاو بیه نه آلشت بکید",
-       "editundo": "ناانجومگر کردن",
+       "editundo": "ناٱنجومگر کردن",
        "diff-empty": "(بی فرق)",
        "diff-multi-sameuser": "({{PLURAL:$1|یه گل نسقه مینجایی|$1 نسقه یا مینجایی}} وه دس{{PLURAL:$2|کاریاری تر|$2 کاریاریا}} نشو دئه نبیه)",
        "diff-multi-otherusers": "({{PLURAL:$1|یه گل نسقه مینجایی|$1 نسقه یا مینجایی}} وه دس{{PLURAL:$2|کاریاری تر|$2 کاریاریا}} نشو دئه نبیه)",
        "diff-multi-manyusers": "({{PLURAL:$1|یه گل وانیری مینجاگرته|$1وانیریا مینجا گرته}} بیشتر د $2 {{PLURAL:$2|کاریار|کاریاریا}} نشو دئه نبیه)",
        "difference-missing-revision": "{{PLURAL:$2|یه گل ویرایشت|$2 ویرایشت}} د فرق مینجا($1) {{PLURAL:$2|پیدا نبی|پیدا نبینه}}.\n\nشایت بانی جاونه وه وا یه گل ویرگار وه هنگوم نبیه که د یه گل بلگه پاکسا بیه هوم پیوند بیه بوئه.\nشایت جزئیات د   [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log]  پیدا بوئن.",
-       "searchresults": "نتيجٱیا پی جۊری",
-       "searchresults-title": "نتيجٱیا پی جۊری سی \"$1\"",
+       "searchresults": "نتيجٱیا پاٛ جۊری",
+       "searchresults-title": "نتيجٱیا پاٛ جۊری سی \"$1\"",
        "titlematches": "داسون بلگه یکی بیه",
        "textmatches": "هومسازی نیسسه بلگه.",
        "notextmatches": "نیسسه بلگه هومسازی ناره",
        "next-page": "بلگه نهایی",
        "prevn-title": "پيشتر $1 {{PLURAL:$1|نتيجه|نتيجيا}}",
        "nextn-title": "نيايی $1 {{PLURAL:$1|نتيجه|نتيجيا}}",
-       "shown-title": "نشۊ دٱئن $1 {{PLURAL:$1|نتیجٱ|نتیجٱیا}} سی هار بلگٱ",
+       "shown-title": "نشوݩ داٛین $1 {{PLURAL:$1|نتیجٱ|نتیجٱیا}} سی هار بٱلگٱ",
        "viewprevnext": "ديئن ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "'''ایچه بلگه ای هئ وه نوم\"[[:$1]]\" که ها د ای ویکی'''",
        "searchmenu-new": "'''ای بلگه نه راس كو \"[[:$1]]\" د ای  ويكي!'''",
        "searchprofile-articles": "بٱلگٱيا مؽنونٱ دار",
-       "searchprofile-images": "وارسگریا خلکمن",
-       "searchprofile-everything": "Ù\87Ù±Ù\85اÙ\9b چی",
+       "searchprofile-images": "ڤارسگرؽا خلکمن",
+       "searchprofile-everything": "Ù\87Ù\85Ù± چی",
        "searchprofile-advanced": "پیشکردٱ",
        "searchprofile-articles-tooltip": "بٱگٱرد مؽن $1",
-       "searchprofile-images-tooltip": "جانیایانه پی جوری کو",
-       "searchprofile-everything-tooltip": "Ù\87Ù\85Ù\87 Ù\85Û\8cÙ\86Ù\88Ù\86Ù\87 Û\8cا Ù\86Ù\87 Ù¾Û\8c Ø¬Ù\88رÛ\8c Ù\83Ù\88 (شاÙ\85Ù\84ا Ø¨Ù\84Ú¯Ù\8aا Ú\86Ù\83 Ú\86Ù\86Ù\87)",
-       "searchprofile-advanced-tooltip": "نوم جايا نوم ديار بٱگٱرد",
-       "search-result-size": "$1 ({{PLURAL:$2|1 Ú©Ù±Ù\84Û\8cÙ\85Ù±|$2 Ú©Ù±Ù\84Û\8cمٱیا}})",
-       "search-result-category-size": "{{PLURAL:$1|1 أندوم|$1 أندومیا}} ({{PLURAL:$2|1 زیردأسە|$2 زیردأسە یا}}، {{PLURAL:$3|1 جانیا|$3 جانیایا}}",
+       "searchprofile-images-tooltip": "جانؽایاناْ پاٛ جۊری کو",
+       "searchprofile-everything-tooltip": "Ù\87Ù\85Ù± Ù\85ؽÙ\86Ù\88Ù\86Ù±Û\8cا Ù\86اÙ\92 Ù¾Û\8a Ø¬Û\8aرÛ\8c Ù\83Ù\88 (شاÙ\85Ù\84اÙ\92 Ø¨Ù±Ù\84Ú¯Ù±Ù\8aا Ú\86Ù±Ú© Ú\86Ù\86Ù±)",
+       "searchprofile-advanced-tooltip": "نوم جايا نوم دؽار بٱگٱرد",
+       "search-result-size": "$1 ({{PLURAL:$2|1 Ú©Ù\84Ù±Ù\85Ù±|$2 Ú©Ù\84Ù±مٱیا}})",
+       "search-result-category-size": "{{PLURAL:$1|1 ٱندوم|$1 ٱندومؽا}} ({{PLURAL:$2|1 زؽردٱسٱ|$2 زؽردٱسٱیا}}، {{PLURAL:$3|1 جانیا|$3 جانیایا}}",
        "search-redirect": "(ڤورگشتن سی $1)",
        "search-section": "(بهرجا $1)",
        "search-category": "(دسه $1)",
        "right-bot": "باور بیه چی یه گل پردازشت خودانجوم",
        "right-nominornewtalk": "حیرده ویرایشت بلگه یا چک چنه وه شکلی که باعث گوتن پیغوم تازه نبوئه.",
        "right-apihighlimits": "سخم بالاتر د وه کار بسن API",
-       "right-writeapi": "د Ù\86Û\8cسÙ\86Ù\86 Ø§Û\8c Ù¾Û\8c Ø¢Û\8c Ù\88Ù\87 Ú©Ø§Ø± Ø¨Ø¦Û\8cرÛ\8cت",
+       "right-writeapi": "د Ù\86Û\8cسٱÙ\86Ù\86 Ø§Ù\9bÛ\8c Ù¾Û\8c Ø¢Û\8c Ú¤ Ú©Ø§Ø± Ø¨Ø§Ù\9bÛ\8cرؽت",
        "right-delete": "بلگیا نه پاکسا کو",
        "right-bigdelete": "بلگه یایی که ویرگار گپی دارن پاکسا بکیت",
        "right-deletelogentry": "پاکسا کردن و ناپاکسا کردن داده واریایی ویجه ای د پهرستنومه",
        "right-managechangetags": "راس کئردئن [[Special:سأردیسیا|سأردیسیا]] پاکسا کئردئن د رئسینە جا",
        "right-applychangetags": "ڤئ کار گئرئتئنئ [[Special:سأردیسیا|سأردیسیا]] ڤاگئرد آلئشتیا ھأرکومئشوٙ.",
        "right-changetags": "Add and remove arbitrary [[Special:Tags|tags]] on individual revisions and log entries",
-       "newuserlogpage": "راس Ø¨Û\8cÙ\87 Ù\88ا کاریار",
+       "newuserlogpage": "دÛ\8fرس Ø¨Û\8cÙ± Ú¤ا کاریار",
        "newuserlogpagetext": "یه پهرستنومه راس بیئن کاریاره",
        "rightslog": "پهرستنومه حقوق کاریار",
        "rightslogtext": "یه پهرستنومه آلشتیا حقوق کاریاره.",
        "nchanges": "$1 {{PLURAL:$1|آلشت|آلشتیا}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|د آخری دیئن}}",
        "enhancedrc-history": "ڤیرگار",
-       "recentchanges": "آلشتؽیا ایسنی",
+       "recentchanges": "آلشتؽا ایسنی",
        "recentchanges-legend": "گوزینٱیا آلشتیا ایسناٛنی",
        "recentchanges-summary": "دو بیشتر آلشتیا تازباو نه د ویکی نه د ای بلگه پیگری کو.",
        "recentchanges-noresult": "هیژ آلشتی د درازا دوره دیار بیه وا ای معیاریا یکی نبی.",
        "recentchanges-feed-description": "دو بیشتر آلشتیا تازباو نه د ویکی که ها د هوال حون پیگری کو.",
-       "recentchanges-label-newpage": "ای ڤیرایشت یاٛ گاٛل بلگٱ تازٱ راس کردٱ.",
-       "recentchanges-label-minor": "Û\8cÙ± Û\8cاÙ\9b Ú¯Ø§Ù\9bÙ\84 Ú¤Û\8cراÛ\8cشت کوچکٱ",
-       "recentchanges-label-bot": "ای ويرايشت نه يه بوت انجوم دئه",
+       "recentchanges-label-newpage": "اؽ ڤیرایش یاٛ بٱلگٱ تازٱ دۏرس کردٱ.",
+       "recentchanges-label-minor": "Û\8cÙ± Û\8cاÙ\9b Ú¤Û\8cراÛ\8cØ´ کوچکٱ",
+       "recentchanges-label-bot": "اؽ ڤيرايش ناْ ياٛ بوت ٱنجوم داٛیٱ",
        "recentchanges-label-unpatrolled": "ای ويرايشت هنی تيه واداشت نبيه",
        "recentchanges-label-plusminus": "انازه بلگه وه شمار ای بایتیا آلشت کرده.",
        "recentchanges-legend-heading": "<strong>میراث:</strong>",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (همچنو بوینیت [[ویجه:بلگیا تازه|نوم گه بلگیا تازه]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (همچنو باٛینؽت [[ڤیژٱ:بٱلگٱیا تازٱ|نوم گٱ بٱلگٱیا تازٱ]])",
        "recentchanges-legend-plusminus": "(<em>±123</em>)",
        "rcnotefrom": "د هار آلشتیا د $2 هیئن(د بال د $1 نشون دئه بیه)",
        "rclistfrom": "آلشتیا تازه ایی که وا $3 $2 شرو بیه نشونش بئه",
        "rcshowhideminor": "ڤیرایشتیا فاٛرٱ کوچک $1",
        "rcshowhideminor-show": "نشو دئن",
        "rcshowhideminor-hide": "قام کردن",
-       "rcshowhidebots": "$1 Ø±Ù\88اتÙ\8aا Û\8cا Ø¨Ù\88تÙ\8aا",
+       "rcshowhidebots": "$1 Ø±Ù\88باتؽا Û\8cا Ø¨Ù\88تؽا",
        "rcshowhidebots-show": "نشۊ دٱئن",
        "rcshowhidebots-hide": "قام کردن",
-       "rcshowhideliu": "$1 Ú©Ø§Ø±Û\8cارÛ\8cا Ø«Ù\88ت Ù\86اÙ\85 Ú©Ø±Ø¯Ù\87",
+       "rcshowhideliu": "$1 Ú©Ø§Ø±Û\8cارÛ\8cا Ø³Ù±Ø¨Øª Ù\86Ù\88Ù\85 Ú©Ø±Ø¯Ù±",
        "rcshowhideliu-show": "نشۊ دٱئن",
        "rcshowhideliu-hide": "قام کئردئن",
        "rcshowhideanons": "کاریار نادیار $1",
        "rcshowhidecategorization-show": "نئشوٙ دأئن",
        "rcshowhidecategorization-hide": "قام کئردئن",
        "rclinks": "آخرین آلشتیا $1 د آخرین رۊزیا دیاری بٱک $2",
-       "diff": "فرق",
+       "diff": "فٱرق",
        "hist": "ڤیرگار",
        "hide": "قام کردن",
        "show": "نشۊ دٱئن",
        "unpatrolledletter": "!",
        "number_of_watching_users_pageview": "[$1 دینه {{PLURAL:$1|کاریار|کاریاریا}}]",
        "rc-change-size": "$1",
-       "rc-change-size-new": "$1 {{PLURAL:$1|بایت|بایتیا}} ناٛها آلشتکاری",
+       "rc-change-size-new": "$1 {{PLURAL:$1|بایت|بایتؽا}} دما آلشتکاری",
        "newsectionsummary": "/* $1 */ بهرجا تازه",
        "rc-enhanced-expand": "جزيات نشون بيئه",
        "rc-enhanced-hide": "جزياته قام كو",
        "rc-old-title": "ذاتا چی \"$1\" راس بیه",
        "recentchangeslinked": "آلشتیا تی یٱکی",
        "recentchangeslinked-feed": "آلشتیا تی یکی",
-       "recentchangeslinked-toolbox": "آلشتیا تاٛ یٱک",
+       "recentchangeslinked-toolbox": "آلشتؽا تاٛ یٱک",
        "recentchangeslinked-title": "آلشتیا تاٛ یکی د $1",
-       "recentchangeslinked-summary": "ای نوم بلگٱ تازٱ د بلگٱیایی کاٛ ۋا بلگٱیا ۋیجٱ هوم پیۋند بینٱ آلشت بیٱ(یا سی ٱندومیا دٱسٱ بٱنی بیٱ)\nبلگٱیایی کاٛ هان د [[Special:Watchlist|your watchlist]]و گٱپ بینٱ",
+       "recentchangeslinked-summary": "اؽ نوم بٱلگٱ تازٱ د بٱلگٱیایی کاْ ڤا بٱلگٱیا ڤیژٱ هوم پاٛڤٱن بینٱ آلشت بیٱ(یا سی ٱندومؽا دٱسٱ بٱنی بیٱ)\nبٱلگٱیایی کاْ هان د [[Special:Watchlist|your watchlist]]ۉ گٱپ بینٱ",
        "recentchangeslinked-page": "نوم بلگٱ:",
        "recentchangeslinked-to": "آلشتیایی که د بلگه یا هوم پیوند بینه وه جا بلگه دئیه بیه نشو بیه",
        "recentchanges-page-added-to-category": "[[:$1]]د دأسە ئضاف بی",
        "recentchanges-page-added-to-category-bundled": "[[:$1]] و {{PLURAL:$2|بألگە تأکی|$2 بألگە یا}} د دأسە ئضاف بییئن",
-       "recentchanges-page-removed-from-category": "[[:$1]] د دٱسٱ جاْگا بی",
+       "recentchanges-page-removed-from-category": "[[:$1]] د دٱسٱ جگا بی",
        "recentchanges-page-removed-from-category-bundled": "[[:$1]] و {{PLURAL:$2|بألگە تأکی|$2 بألگە یا}} د دأسە ئضاف بییئن",
        "autochange-username": "آلئشتکاری خودأنجوم مئدیاڤیکی",
-       "upload": "سوڤار کردن جانیا",
+       "upload": "سڤار کردن جانؽا",
        "uploadbtn": "سوڤار کئردئن جانیا",
        "reuploaddesc": "سوار کردن نه انجوم شیو بکیت و د ورئردیت جابلگ سوارکرد",
        "upload-tryagain": "کل کردن توضیحیا آلشت دئیه بیه جانیا",
        "listfiles-latestversion": "نسقه تازه",
        "listfiles-latestversion-yes": "هأری",
        "listfiles-latestversion-no": "تە",
-       "file-anchor-link": "جانیا",
-       "filehist": "ڤیرگار جانیا",
-       "filehist-help": "ری  ويرگاريا بپورنيت تا نسقه مرتوط بونيت.",
+       "file-anchor-link": "جانؽا",
+       "filehist": "ڤیرگار جانؽا",
+       "filehist-help": "ری  ڤيرگارؽا بٱپۊرنؽت تا نۏسخٱ مٱربۊتاْ باٛینؽت.",
        "filehist-deleteall": "هأمە نئ پاکسا کو",
        "filehist-deleteone": "پاکسا کئردئن",
        "filehist-revert": "لئرنیئن",
-       "filehist-current": "تازٱ باڤ",
+       "filehist-current": "تازٱ با",
        "filehist-datetime": "ڤيرگار/ڤٱخت",
-       "filehist-thumb": "ٱسگ کوچک بیٱ",
-       "filehist-thumbtext": "كوچک کردن سی نوسقٱ چی $1",
+       "filehist-thumb": "عٱسک کوچک بیٱ",
+       "filehist-thumbtext": "كوچک کردن سی نۏسخٱ چی $1",
        "filehist-nothumb": "هیچ بن کلیکی نئ",
        "filehist-user": "کاریار",
        "filehist-dimensions": "اۊیٱکؽا",
        "filehist-filesize": "انازه فایل",
        "filehist-comment": "ڤیر ۉ باڤٱر",
-       "imagelinks": "د کار گرتن جانیا",
-       "linkstoimage": "دمال بيه {{PLURAL:$1|ديس ونيا بلگه|$1 ديس ون بلگيا}} دای فایل:",
-       "linkstoimage-more": "بیشتر د $1 بلگه د ای جانیا هوم پیوند {{PLURAL:$1|بیه|بینه}}.\nنومگه هاری تئنا {{PLURAL:$1|اولین هوم پیوند|اولین $1 هوم پیوند}} د ای بلگه نه نشو می ئه.\n[[Special:WhatLinksHere/$2|نومگه کامل]] ئم هیئش.",
-       "nolinkstoimage": "ایچه هیژ بلگه ای سی هوم پیوند بیئن وا ای جانیا نی",
+       "imagelinks": "ڤ کار گرتن جانؽا",
+       "linkstoimage": "دۏنبال بيٱ {{PLURAL:$1|ديس ڤنؽا بٱلگٱ|$1 ديس ڤنؽا بٱلگٱيا}} د اؽ فایلٛ:",
+       "linkstoimage-more": "بؽشتر د $1 بٱلگٱ د اؽ جانؽا هوم پاٛڤٱن {{PLURAL:$1|بٱ|بینٱ}}.\nنومگٱ هاری تٱنڳؽا{{PLURAL:$1|ٱڤلی هوم پاٛڤٱن|ٱڤلی $1 هوم پاٛڤٱن}} د ؽ بٱلگٱ ناْ نشوݩ مؽ یٱ.\n[[Special:WhatLinksHere/$2|نومگٱ کامل]] ٱم هؽسش.",
+       "nolinkstoimage": "ایچاْ هیچ بٱلگاٛیی سی هوم پیاٛڤٱن بیین ڤا اؽ جانؽا نؽ",
        "morelinkstoimage": " [[ویجه:چه هوم پیوندی ها ایچه/$1|هوم پیوندیا هنی]]سی ای جانیا نه بونیت.",
        "linkstoimage-redirect": "$1 (واگردونی جانیا) $2",
        "duplicatesoffile": "{{PLURAL:$1|جانیا|جانیایا}} هاری نسقه تکراری ای جانیا {{PLURAL:$1|هئ|هئن}} ([[Special:FileDuplicateSearch/$2|دونسمنیا هنی]]):",
        "sharedupload": "ای جانیا که د $1 هئ شایت د پروجه یا هنی استفاده بیه.",
        "sharedupload-desc-there": "ای جانیا ها د $1و شایت د پروجه یا هنی وه کار گرته بوئه.\nسی دونسمنیا هنی لطفن [$2 بلگه توضیحیا جانیا] نه سیل بکیت.",
-       "sharedupload-desc-here": "فایلؽ کاْ د $1 شایٱد د مؽن پروجٱیا هٱنی اْستفادٱ بیٱ.\nتۉزیی د بارٱ[$2 file description page] د هار نشو داٛیٱ بیٱ",
+       "sharedupload-desc-here": "فایلٛؽ کاْ د $1 شایٱد د مؽن پرۉژٱیا هنی اْستفادٱ بیٱ.\nتۉزیی دبارٱ[$2 file description page] د هار نشوݩ داٛیٱ بیٱ",
        "sharedupload-desc-edit": "ای جانیا د $1 ئه و می تونه د پروجه یا هنی وه کار گرت بوئه.\nار حاستیت می تونیت توضیحیا حانیا نه د [$2 بلگه توضیحیا خوش] د اوچه ویرایشت بکیت.",
        "sharedupload-desc-create": "ای جانیا د $1 ئه و می تونه د پروجه یا هنی وه کار گرت بوئه.\nار حاستیت می تونیت توضیحیا حانیا نه د [$2 بلگه توضیحیا خوش] د اوچه ویرایشت بکیت.",
        "filepage-nofile": "چنو فایلی وا ای نوم نئ.",
        "unusedtemplates": "قالویا وه کار نبرده بیه.",
        "unusedtemplatestext": "ای بلگه همه بلگه یایی که هان د نومجا {{ns:template}}و د هیچ بلگه ای وه کار گرته نبینه، نوم بنی می که.\nد ویرتو با که دما یه که بهایت بلگه یا هوم پیوند هنی نه پاکسا بکیت دشو وارسی ای انجوم بئیت.",
        "unusedtemplateswlh": "هوم پیوندیا هنی",
-       "randompage": "بÙ\84Ú¯Ù± Ø¨Ø®Øªکی",
+       "randompage": "بٱÙ\84Ú¯Ù± Ø¨Ù±Ø®ØªÙ±کی",
        "randompage-nopages": "هیچ بلگه ای د ای {{PLURAL:$2|نومجا|نومجایا}}  نئ:$1.",
        "randomincategory": "بلگه بختی د ای دسه",
        "randomincategory-invalidcategory": "\"$1\"  نوم دسه معتوری نئ.",
        "withoutinterwiki-legend": "پیشون",
        "withoutinterwiki-submit": "نشون دائن",
        "fewestrevisions": "بلگه یایی که کمتری وانئری نه دارن",
-       "nbytes": "$1{{PLURAL:$1|بايت|بایتیا}}",
+       "nbytes": "$1{{PLURAL:$1|بايت|بایتؽا}}",
        "ncategories": "$1{{PLURAL:$1|دسه|دسه يا}}",
        "ninterwikis": "$1 {{PLURAL:$1|مئن ویکی|مئن ویکیا}}",
        "nlinks": "$1 {{PLURAL:$1|هوم پیوند|هوم پیوندیا}}",
        "listusers-desc": "سرجاخودگری د اساس گپ د کؤچک",
        "usereditcount": "$1{{PLURAL:$1|ویرایشت|ویرایشتیا}}",
        "usercreated": "{{جنسیت:$3|راس بیه}}د $1 at $2",
-       "newpages": "بÙ\84Ú¯Ù±Û\8cا Ù\86Û\8f",
+       "newpages": "بٱÙ\84Ú¯Ù±Û\8cا Ù\86Û\8a",
        "newpages-username": "نوم کاریاری:",
        "ancientpages": "بلگه یا نهاتر",
        "move": "جاوه جا بوئيت",
        "apihelp": "هومیاری آی پی آی",
        "apihelp-no-such-module": "ماجول \"$1\" پیدا نبی.",
        "booksources": "سرچشمه یا كتاو",
-       "booksources-search-legend": "پاٛ جۊری سی سٱرچٱشمٱیا کتاو",
+       "booksources-search-legend": "پاٛ جۊری سی سٱرچشمٱیا کتاو",
        "booksources-isbn": "آی اس بی ان:",
        "booksources-search": "پاٛ جۊری",
        "booksources-text": "د هار نومگه ای د هوم پیوندیا د دیارگه یا هنی اومائه که کتاویا نو و دس دوئم می فروشن، و همچنو شایت دونسمنیا بیشتری راجع وه کتاو حاستنی شما داشتوئن:",
        "prevpage": "بلگه دمایی($1)",
        "allpagesfrom": "بلگه یای که د شرو بینه نشو بیه:",
        "allpagesto": "بلگه یایی که د تموم بینه نشو بیه.",
-       "allarticles": "هٱماٛ بٱلگٱيا",
+       "allarticles": "هٱمٱ بٱلگٱیا",
        "allinnamespace": "همه بلگه یا($1 نوم جا)",
-       "allpagessubmit": "رÛ\8f",
+       "allpagessubmit": "رÛ\89",
        "allpagesprefix": "بلگه یایی که پس نوم دارن نشو بیه:",
        "allpagesbadtitle": "عنوان بلگه حاسته بیه معتور نی،یا  یه گل مئن زونی یا مئن ویکی عنوان غلطه.\nیه شایت شومل یکی با یا بیشتر کاراکتریا نبوئه که سی ای موضوعیا استفاده بوئن",
        "allpages-bad-ns": "{{نوم دیارگه}} د ای نوم جا نئ \"$1\".",
        "listgrouprights-namespaceprotection-restrictedto": "دسرسیا مجاز کاریار سی ویرایشت",
        "trackingcategories": "دئماگئری دأسە یا",
        "trackingcategories-summary": "ای بلگه نومگه دسه یایی دماگری بیه ئه که وه شکل خودانجوم وه دس ویکی وارسگر پر بوئن . نومیا ونو نها آلشت کردن پیغومیا سامونه ای مرتوط د نومجا {{ns:8}} آلشت دئه با.",
-       "trackingcategories-msg": "ردگیری دٱسٱ",
+       "trackingcategories-msg": "رٱدگیری دٱسٱ",
        "trackingcategories-name": "نوم پئیغوم",
        "trackingcategories-desc": "جادیارکنیا گنجایشت دسه",
        "noindex-category-desc": "ای بلگه وا رباتیا نومگه کاری نبیه و سی یه کلیمه یا جادویی <code><nowiki>__NOINDEX__</nowiki></code> د وه یا د جاگه ای که بیرق مجازه دش هئ.",
        "deleteprotected": "شما نمی تونیت ای بلگه نه پاکسا بکیت سی یه که وه پر و پیم بیه.",
        "deleting-backlinks-warning": "''' هشدار:''' [[Special:WhatLinksHere/{{FULLPAGENAME}}|بلگه یا هنی]] ین که وه بلگه یی که شما د حال و بار پاکسا کردن ونیت پیوند دارن یا د وه پرگنجایشت کاری بیینه.",
        "rollback": "چواشه کردن ویرایشتیا",
-       "rollbacklink": "ڤرگٱشتݩ",
+       "rollbacklink": "ڤرگٱشتن",
        "rollbacklinkcount": "چواشه کردن $1 {{PLURAL:$1|ویرایشت|ویرایشتیا}}",
        "rollbacklinkcount-morethan": "چواشه کردن بیشتر د$1 {{PLURAL:$1|ویرایشت|ویرایشتیا}}",
        "rollbackfailed": "چواشه کردن د خوئی انجوم نبی",
        "protect-locked-dblock": "سی یه که رسینه جا قلف بیه، امکان آلشت دئن ریتراز پر و پیم کاری بلگه یا د ایسه نئ.\nمیزونکاری ایسنی بلگه '''$1''' ها دی ای قرار:",
        "protect-locked-access": "حساو کاریاری شما سی آلشتکاری ریتراز پر و پیم کاری صلاداری ناره.\nمیزونکاریا ایسنی بلگه '''$1''' ها دی ای قرار:",
        "protect-cascadeon": "ای بلگه ایسنی پر و پیم کاری بیه، سی یه که د {{PLURAL:$1|بلگه|بلگه یا}} هاری که گزینه پر و پیم کاری تاف نمایی {{PLURAL:$1|وه|ونو}} کنشتکاره، اومائه.\nآلشتیایی که مال ریتراز ای بلگه ن ری پر و پیم کاری تاف نمایی کارگرایی نارن.",
-       "protect-default": "ھٱماٛ کاریاریا سلا دارٱن",
+       "protect-default": "ھٱمٱ کاریارؽا سلا دارٱن",
        "protect-fallback": "فقط کاریاریایی که وه «$1» دسرسی دارن، صلادار ای کارن",
        "protect-level-autoconfirmed": "فأقأط کاریاریا خود پوشت راس کاری بییە صئلا دارئن",
        "protect-level-sysop": "فأقأط سأردیڤوٙنکاریا",
        "undelete-error-long": "د گات زنه کردن جانیا یه گل خطا پیش اوما:\n\n\n$1",
        "undelete-show-file-confirm": "آیا یه دل بئیته که میهایت یه گل نسقه پاکسا بیه د جانیا \"<nowiki>$1</nowiki>\" که ها د ویرگار $2 ساعت $3 نه سیل بکیت؟",
        "undelete-show-file-submit": "هأری",
-       "namespace": "نوم جا:",
-       "invert": "Ú¯Ù\88Ù\84Ú¤Ù\88رÚ\86Û\8c Ø¨Û\8cئÙ\86 Ø¨Ù±Ø±Ù±Ø³Ú¯ Ø¨Û\8aٱ",
-       "tooltip-invert": "د ری ای جعوه بپورنیت و آلشتیایی نه که د مینجا نوم ورگه انتخاو بیه انجوم بینه قام بکیت(و ار نوم ورگه شریکی وارسی بیه)",
+       "namespace": "نوم جا",
+       "invert": "Ú¯Ù\84Ù\9bٱڤرÚ\86Û\8c Ø¨Û\8cÛ\8cÙ\86 Ø¨Ù±Ø±Ø¹Ù±Ø³Ú© Ø¨Û\8aئٱ",
+       "tooltip-invert": "د ری اؽ جٱڤٱ بٱپۊرنؽت ۉ آلشتؽایؽ ناْ کاْ د مؽنجا نوم ڤرگٱ اْنتخاو بیٱ ٱنجوم بینٱ قایم بٱکؽت(ۉ ٱر نوم ڤرگٱ شریکی ڤارسی بیٱ)",
        "tooltip-whatlinkshere-invert": "ای جعون نه سی نهو کردن هوم پیوند بلگه یایی که نوم جاشو انتخاو بیه، انتخاو بکیت.",
-       "namespace_association": "نوم جایا یکاگرته",
-       "tooltip-namespace_association": "ای جعوه نه وارسی بکیت ای جعوه د ور گرته چک چنه یا داسون نوم ورگه شریکی و نوم ورگه انتخاو بیه ئه",
+       "namespace_association": "نوم جایا یٱکاگرتٱ",
+       "tooltip-namespace_association": "اؽ جٱڤٱ ناْ ڤارسی بٱکؽت اؽ جٱڤٱ د ڤٱر گرتٱ چٱک چنٱیا داسوݩ نوم ڤرگٱ شریکی ۉ نوم ڤرگٱ اْنتخاو بیٱ آ",
        "blanknamespace": "ٱسلی",
        "contributions": "{{GENDER:$1|کاریار}} هومیاریا",
        "contributions-title": "ھومیاری كاریار سی $1",
        "uctop": "تازه باو",
        "month": "د ما(یا زیتر)",
        "year": "د سال",
-       "sp-contributions-newbies": "Ù\81Ù±Ù\82ٱت Ù\87Ù\88Ù\85Û\8cارÛ\8cاÛ\8cÛ\8c کاْ د هساو تازٱ بیٱ نشوݩ باٛیٱ",
+       "sp-contributions-newbies": "Ù\81Ù\82ٱت Ù\87Ù\88Ù\85Û\8cارؽاÛ\8cؽ کاْ د هساو تازٱ بیٱ نشوݩ باٛیٱ",
        "sp-contributions-newbies-sub": "سی حساویا تازه",
        "sp-contributions-newbies-title": "هومیاریا کاریار سی حساویا تازه",
        "sp-contributions-blocklog": "پهرستنۊمٱ قولف بیٱ",
        "sp-contributions-userrights": "دیوونداری حقوق کاریار",
        "sp-contributions-blocked-notice": "د دسرسی ای کاریا د ایسنی نهاگری بیه.\nآخری برشت د پهرستنومه نهاگری ها د سرچشمه هاری:",
        "sp-contributions-blocked-notice-anon": "ای آی پی ایسنی دسرسی ناره.\nآخری برشت د پهرستنومه ها د سرچشمه هاری:",
-       "sp-contributions-search": "سی هومیاریا پاٛ جۊر با",
+       "sp-contributions-search": "سی هومیارؽا پاٛ جۊر با",
        "sp-contributions-username": "نوم نشوݩ آی پی يا نوم كارڤٱری:",
-       "sp-contributions-toponly": "Ù\81Ù\82ٱت Ú¤Û\8cراÛ\8cشتؽاÛ\8cÛ\8c Ú©Ø§Ù\92 Ø¬Û\8fزڤاÙ\92 Ø¢Ø®Ø±Û\8cÝ© Ø¯Û\89ران نشو باٛیٱ",
-       "sp-contributions-newonly": "Ù\81Ù±Ù\82ٱت Ú¤Û\8cراÛ\8cشتÛ\8cاÛ\8cÛ\8c Ú©Ø§Ù\92 Ù\87ؽÙ\86 Ø¯Û\8fرس Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\86 Ù\86Ø´Ù\88 باٛیٱ.",
+       "sp-contributions-toponly": "Ù\81Ù\82ٱت Ú¤Û\8cراÛ\8cشؽاÛ\8cÛ\8c Ú©Ø§Ù\92 Ø¬Û\8fز Ø¢Ø®Ø±Û\8c Ø¯Û\89رٱن نشو باٛیٱ",
+       "sp-contributions-newonly": "Ù\81Ù\82ٱت Ú¤Û\8cراÛ\8cشؽاÛ\8cؽ Ú©Ø§Ù\92 Ù\87ؽÙ\86 Ø¯Û\8fرس Ú©Ø±Ø¯Ù\86 Ø¨Ù±Ù\84گاÙ\86 Ù\86Ø´Ù\88Ý© باٛیٱ.",
        "sp-contributions-submit": "پاٛ جۊری",
-       "whatlinkshere": "کوم هوم پیۋندیا هان ایچاٛ",
+       "whatlinkshere": "کوم هوم پاٛڤٱنؽا هان ایچاْ",
        "whatlinkshere-title": "بلگه ای که د $1 هوم پیوند بیه",
        "whatlinkshere-page": "بلگٱ",
        "linkshere": "بلگیا نهایی د '''$2''' هوم پیوند بیه",
        "blocklist-nousertalk": "نبوئه بلگه چک چنه خوتونه ویرایشت بکید",
        "ipblocklist-empty": "جاگه نوم گه حالیه",
        "ipblocklist-no-results": "دسرسی نوم کاریاری یا تیرنشون آی پی حاسته بیه نهاگری نبیه.",
-       "blocklink": "Ù\86Ù\87اگرÛ\8c Ø¨Û\8aٱ",
+       "blocklink": "Ù\86Ù\88ئاگرÛ\8c Ø¨Û\8aئٱ",
        "unblocklink": "بی قطی",
        "change-blocklink": "اجازه نديئن سی  آلشت",
        "contribslink": "هومیاریا",
        "delete_and_move_text": "== پاکساکاری میها ==\n\nگوتار ها د مقصد «[[:$1]]» . آیا میهایت ونه پاکسا بکیت  تا جا وه جاکاری دروس بوئه؟",
        "delete_and_move_confirm": "هری بلگه نه پاکسا کو",
        "delete_and_move_reason": "پاکساکاری سی ممکن بیین جا وه جایی «[[$1]]»",
-       "selfmove": "داسÙ\88Ù\86ا Ø¨Ù\84Ú¯Ù\87 Ø§Ù\88Ù\84 Ù\88 Ø¨Ù\84Ú¯Ù\87 Ù\85Ù\82صد Û\8cÚ©Û\8c Û\8cÙ\86Ø\9b\nÙ\86بÙ\88ئÙ\87 Ø¨Ù\84Ú¯Ù\87 Ù\86Ù\87 Ø¯ Ø®Ù\88Ø´ Ø¬Ø§ Ù\88Ù\87 Ø¬Ø§کاری کرد.",
+       "selfmove": "داسÙ\88Ù\86ؽا Ø¨Ù±Ù\84Ú¯Ù± Ù±Ú¤Ù±Ù\84 Û\89 Ø¨Ù±Ù\84Ú¯Ù± Ù\85Ù±Ù\82سٱد Û\8cٱکؽ Ù\87ؽسÙ\86Ø\9b\nÙ\86Ù\85Û\8aئٱ Ø¨Ù±Ù\84Ú¯Ù±Ù\86اÙ\92 Ú¤ Ø®Ù\88Ø´ Ø¬Ø§ Ú¤ Ø¬Ø§ کاری کرد.",
        "immobile-source-namespace": "نبوئه بلگه یا نه وه نومجا \"$1\" جا وه جا با",
        "immobile-target-namespace": "نبوئه بلگه یا نه وه نومجا \"$1\" جا وه جا با",
        "immobile-target-namespace-iw": "هوم پیوند مینجاویکی حاستنی مجازی سی جا وه جا کردن بلگه نئ.",
        "tooltip-pt-preferences": "اولۋیتیا شوما",
        "tooltip-pt-watchlist": "نوم نوشت د بلگه يايی كه شما آلشتاشونه پيگئری  ميكيد",
        "tooltip-pt-mycontris": "یاٛ گاٛل د هومیاریا شوما",
-       "tooltip-pt-login": "ایما مۊئیم کاٛ رۊئیت ڤامین سامۊنگٱ؛ ڤلی ای کار اٛژبار ینی.",
+       "tooltip-pt-login": "اؽما مۉئیم کاْ رۉیت ڤامؽن سامونگٱ؛ ڤلی اؽ کار اْجباری نؽ.",
        "tooltip-pt-logout": "د سامونه دراومائن",
-       "tooltip-pt-createaccount": "Ø´Ù\88Ù\85ا ØªØ´Ú¤Û\8cÙ\82 Ø¨Û\8cتٱ Ú©Ø§Ù\9b Û\8cاÙ\9b Ú¯Ø§Ù\9bÙ\84 Ù\87ساڤ Ø±Ø§Ø³Øª Ø¨Ú©Û\8cت Ù\88 Ø¨Û\8cاÛ\8cت Ú¤Ø§Ù\85Û\8cÙ\86Ø\9b Ø¯ Ù\87ر Ø¬Û\8aر Ø§Û\8c Ú©Ø§Ø± Ø§Ù\9bÚ\98بارÛ\8c Ù\86Û\8c.",
-       "tooltip-ca-talk": "قسٱ دائبارٱ مینۊنٱ بلگٱ.",
-       "tooltip-ca-edit": "ڤیرایشت ای بلگٱ",
-       "tooltip-ca-addsection": "د یه گل بهرجا هنی شرو بک",
+       "tooltip-pt-createaccount": "Ø´Ù\85ا ØªÙ±Ø´Ú¤Û\8cÙ\82 Ø¨Û\8cتٱ Ú©Ø§Ù\92 Û\8cاÙ\9b Ù\87ساÙ\88 Ø¯Û\8fرس Ø¨Ù±Ú©Ø½Øª Û\89 Ø¨Ø½Ø§Ø½Øª Ú¤Ø§Ù\85ؽÙ\86Ø\9b Ú¤Ù\84Û\8c Ø§Ø½ Ú©Ø§Ø± Ø§Ù\92جبارÛ\8c Ù\86ؽ.",
+       "tooltip-ca-talk": "سالفٱ دبارٱ مینونٱ بٱلگٱ.",
+       "tooltip-ca-edit": "ڤیرایش ایٛ بٱلگٱ",
+       "tooltip-ca-addsection": "د یاٛ بهرجا هنی شرۊ بٱک",
        "tooltip-ca-viewsource": "ای بلگه پر و پیم بيه.\nشما تونيت سرچمه ش بئوينيت",
-       "tooltip-ca-history": "دÙ\88ؤرٱ دیین اؽ بٱلگٱ",
+       "tooltip-ca-history": "دÙ\88ئرٱ دیین اؽ بٱلگٱ",
        "tooltip-ca-protect": "ای بلگه نه حفاظت بكيد",
        "tooltip-ca-unprotect": "پر و پیم گیری د ای بلگه نه آلشت بکیت",
        "tooltip-ca-delete": "ای بلگه نه پاکسا کو",
        "tooltip-ca-undelete": "د نو زنه کردن ویرایشتیا ری ای بلگه دما یه که پاکساگری بان",
        "tooltip-ca-move": "ای بگله نه جا وه جا كو",
-       "tooltip-ca-watch": "اضاف کردن ای بلگه وه نوم نوشت پیگئریاتو",
+       "tooltip-ca-watch": "اْزاف کردن اؽ بٱلگٱ ڤ نوم نڤشت پاٛگیریاتو",
        "tooltip-ca-unwatch": "ورداشتن ای بلگه وه نوم نوشت پیگئریاتو",
        "tooltip-search": "پاٛ جۊری {{SITENAME}}",
-       "tooltip-search-go": "رو دبلگٱیی کاٛ یٱ نوم راستکی ها مینش ٱلڤت ٱر دش بۊٱ",
-       "tooltip-search-fulltext": "بÙ\84Ú¯Ù±Û\8cاÙ\86اÙ\9b Ø³Û\8c Ú\86اÙ\9bÙ\86Û\8c Ù\86Û\8cسسٱÛ\8cÛ\8c Ù¾Û\8c Ø¬Û\8aرÛ\8c Ø¨Ù±Ú©.",
-       "tooltip-p-logo": "ساٛیلٛ سٱرآسونٱ بٱکؽت",
-       "tooltip-n-mainpage": "سرآسۊنٱ نٱ ساٛیل بٱکیت",
-       "tooltip-n-mainpage-description": "سٱرآسونٱ ناْ ساٛیلٛ بٱکؽت",
-       "tooltip-n-portal": "دبارٱ پروجٱ؛ شما مؽتونؽت(تونؽت) چؽ بٱکؽت؛ د کوجا اؽ چیاناْ بٱجۊرؽت.",
-       "tooltip-n-currentevents": "ساڤند دۊنسمنیایی کاٛ هان د روخ ڤنیا تازٱ باڤ دیاری بٱک",
-       "tooltip-n-recentchanges": "یاٛ گاٛل نومگٱ سی آلشتکاریا د ڤیکی",
-       "tooltip-n-randompage": "سوۋار کرد بلگٱ بختکی",
-       "tooltip-n-help": "یاٛ گاٛل جاگٱ سی فٱماٛسن",
-       "tooltip-t-whatlinkshere": "یاٛ Ú¯Ø§Ù\9bÙ\84 Ù\86Ù\88Ù\85Ú¯Ù± Ø¯ Ù\87Ù\85Ù± Ø¨Ù\84Ú¯Ù±Û\8cاÛ\8cÛ\8c Ú©Ø§Ù\9b Ø§Û\8cÚ\86اÙ\9b Ù\87Ù\88Ù\85 Ù¾Û\8cÚ¤Ù\86د Ø¯Ø§Ø±Ù\86.",
-       "tooltip-t-recentchangeslinked": "آلشتیا تازٱ باڤ ماٛن بلگٱیایی کاٛ د ای بلگٱ هوم پیڤند بینٱ",
+       "tooltip-search-go": "رۉ د بٱلگاٛیؽ کاْ یٱ نوم روسی ها مؽنش ٱلڤٱت ٱر دش بۊئٱ",
+       "tooltip-search-fulltext": "بٱÙ\84Ú¯Ù±Û\8cاÙ\86اÙ\92 Ø³Û\8c Ú\86Ù\86Û\8c Ù\86Û\8cسساÙ\9bÛ\8cؽ Ù¾Ø§Ù\9b Ø¬Û\8aرÛ\8c Ø¨Ù±Ú©Ù\88.",
+       "tooltip-p-logo": "ساٛلٛ سرآسونٱ بٱکؽت",
+       "tooltip-n-mainpage": "سرآسونٱ ناْ ساٛلٛ بٱکؽت",
+       "tooltip-n-mainpage-description": "سرآسونٱ ناْ ساٛلٛ بٱکؽت",
+       "tooltip-n-portal": "دبارٱ پرۉژٱ؛ شما مؽ تونؽت چؽ بٱکؽت؛ د کوجا اؽ چیاناْ بٱجۊرؽت.",
+       "tooltip-n-currentevents": "ساڤند دونسمنیایؽ کا هان د روخ ڤنؽا تازٱ باڤ دؽاری بٱک",
+       "tooltip-n-recentchanges": "یاٛ نومگٱ سی آلشتکاریا د ڤیکی",
+       "tooltip-n-randompage": "سڤار کرد بٱلگٱ بٱختٱکی",
+       "tooltip-n-help": "یاٛ جاگٱ سی فٱمسن",
+       "tooltip-t-whatlinkshere": "یاٛ Ù\86Ù\88Ù\85Ú¯Ù± Ø¯ Ú©Ù\88Ù\84Ù\9b Ø¨Ù±Ù\84Ú¯Ù±Û\8cاÛ\8cÛ\8c Ú©Ø§Ù\92 Ø§Û\8cÚ\86اÙ\92 Ù\87Ù\88Ù\85 Ù¾Ø§Ù\9bÚ¤Ù±Ù\86 Ø¯Ø§Ø±Ù±Ù\86.",
+       "tooltip-t-recentchangeslinked": "آلشتؽا تازٱ مؽن بٱلگٱیایی کاْ د اؽ بٱلگٱ هوم پاٛڤٱن بینٱ",
        "tooltip-feed-rss": "هوال حون آر اس اس سی ای بلگه",
-       "tooltip-feed-atom": "حوال هون اتمی سی ای بلگه",
+       "tooltip-feed-atom": "هڤال هۊن ٱتومی سی اؽ بٱلگٱ",
        "tooltip-t-contributions": "یاٛ گاٛل سیائٱ هومیاری سی {{GENDER:$1|ای کاریار}}",
-       "tooltip-t-emailuser": "سی ای كارور ايميل كل كو",
+       "tooltip-t-emailuser": "سی اؽ كاریار ايماٛیل کلٛ كو",
        "tooltip-t-info": "دونسمنیا بیشتر دباره ای بلگه",
-       "tooltip-t-upload": "سوڤار کردن جانیایا",
-       "tooltip-t-specialpages": "مؽناْ هٱماٛ بٱلگٱیا ڤیجٱ",
-       "tooltip-t-print": "نوسقٱ پاٛلا بیاٛنی سی ای بلگٱ",
-       "tooltip-t-permalink": "هوم پیڤند همیشٱیی سی دوڤارٱ دیاٛن ای بلگٱ",
-       "tooltip-ca-nstab-main": "دياٛن مینۊنٱ بلگٱ",
+       "tooltip-t-upload": "سڤار کردن جانؽایا",
+       "tooltip-t-specialpages": "مؽناْ هٱمٱ بٱلگٱیا ڤیژٱ",
+       "tooltip-t-print": "نۏسخٱ پاٛلا بی ینی سی اؽ بٱلگٱ",
+       "tooltip-t-permalink": "هوم پاٛڤٱن همیشاٛیی سی دوئارٱ دیین اؽ بٱلگٱ",
+       "tooltip-ca-nstab-main": "ديین مؽنونٱ بٱلگٱ",
        "tooltip-ca-nstab-user": "دياٛن بلگٱ کاریار",
        "tooltip-ca-nstab-media": "دیئن بلگه وارسگر",
-       "tooltip-ca-nstab-special": "یٱ یاٛ گاٛل بلگٱ ڤیجٱ یٱ؛ نبۊٱ ڤیرایشتش بٱکیت",
+       "tooltip-ca-nstab-special": "یٱ یاٛ بٱلگٱ ڤیژٱ آ؛ نمۊئٱ ڤیرایشش بٱکؽت",
        "tooltip-ca-nstab-project": "ديئن بلگه پروجه",
-       "tooltip-ca-nstab-image": "ديئن بلگه جانیا",
+       "tooltip-ca-nstab-image": "ديین بٱلگٱ جانؽا",
        "tooltip-ca-nstab-mediawiki": "دیاٛن پیغوم سامۊنٱ",
        "tooltip-ca-nstab-template": "ديئن چۊٱ",
        "tooltip-ca-nstab-help": "ديئن بلگه هومیاری",
-       "tooltip-ca-nstab-category": "ديئن بلگه دسه بنی",
+       "tooltip-ca-nstab-category": "ديین بٱلگٱ دٱسٱ بٱنی",
        "tooltip-minoredit": "یه نه د عنوان حیرده ویرایشت ثوت کو",
        "tooltip-save": "آلشتیاتۊنٱ اٛمایٱ بٱکیت",
        "tooltip-preview": "پیش سیل آلشتیاتو،لطف بکیت وه نونه دما د اماییه کاریشو وه کار بیئریت!",
        "tooltip-watchlistedit-raw-submit": "وه هنگوم سازی سیل برگ",
        "tooltip-recreate": "د نو راس کردن بلگه بی یه که و پاکساگری دماتر وه سیل بکیم",
        "tooltip-upload": "شرو د سوار کرد",
-       "tooltip-rollback": "\"ڤرئشتن\" لرستن د هال وبال ٱڤل سی ای بلگٱ سی یٱ کاٛ هومیاری نیایی بیتر کاری بیٱ ڤا یاٛ گاٛل پۊرنین.",
+       "tooltip-rollback": "\"ڤرگٱشتن\" لٛرسن د هال و بال ٱڤٱل سی اؽ بٱلگٱ سی یٱ کاْ هومیاری نؽایی بؽتر کاری بیٱ ڤا یاٛ پۊرنین.",
        "tooltip-undo": "انجوم نگرتن ای ویرایشت ورگن و همه فرمیا ویرایشت تانه که حالت پیش سیل واکو.یه اجازه میئه سی اضاف کردن یه دلیل د چکسته.",
        "tooltip-preferences-save": "اولويتيا نه ذخيره بكيد",
        "tooltip-summary": "يه چكسته كؤچك وارد بكيد",
        "pageinfo-recent-authors": "شماره کلی نویسنه یا یکونه",
        "pageinfo-magic-words": "جادویی{{PLURAL:$1|کلیمه|کلیمه یا}} ($1)",
        "pageinfo-hidden-categories": "$1{{PLURAL:$1|دسه|دسه يا}} قام بیه",
-       "pageinfo-templates": "{{PLURAL:$1|چۊٱ|چۊٱ یا}} ڤ کار گرتاْ بیٱ ($1)",
+       "pageinfo-templates": "{{PLURAL:$1|چۊٱ|چۊٱ یا}} ڤ کار گرتٱ بیٱ ($1)",
        "pageinfo-transclusions": "{{PLURAL:$1|بلگه|بلگه یا}} وه کار گرته بیه د ($1)",
-       "pageinfo-toolboxlink": "دونسمٱنیا بٱلگٱ",
+       "pageinfo-toolboxlink": "دونسمٱنیٛا بٱلگٱ",
        "pageinfo-redirectsto": "واگردونی سی",
        "pageinfo-redirectsto-info": "دونسمنیا",
        "pageinfo-contentpage": "اشمارده بیه وه عنوان مینونه بلگه",
        "widthheight": "$1 × $2",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|بلگه|بلگه یا}}",
        "file-info": "انازه جانیا: $1, MIME type: $2",
-       "file-info-size": "$1 × $2 پیکسل, انازه فایل: $3, MIME نوع: $4",
+       "file-info-size": "$1 × $2 پیکسل, ٱندازٱ فایل: $3, MIME نوع: $4",
        "file-info-size-pages": "$1 × $2 pixels, انازه جانیا: $3, MIME type: $4, $5 {{PLURAL:$5|بلگه|بلگه یا}}",
        "file-nohires": "عسك ون بالاتري دش ني",
        "svg-long-desc": "جانیا اٛس ۋی جی, نومی $1 × $2 پیکسل, ٱنازٱ جانیا: $3",
        "svg-long-desc-animated": "جانیا جمشدار اس وی جی .نومنا $1 × $2 پيكسل،انازه جانیا:$3",
        "svg-long-error": "جانیا اس وی جی نامعتور:$1",
-       "show-big-image": "جانیا اصلی",
-       "show-big-image-preview": "انازه ای پیش سیل:$1.",
-       "show-big-image-other": "Ù\87Ù\86Û\8c{{PLURAL:$2|Ú¯Ù¾ Ù\86Ù\85ا Ú©Ø±Ø¯Ù\86|Ú¯Ù¾ Ù\86Ù\85ا Ú©Ø±Ø¯Ù\86Û\8cا}}: $1.",
+       "show-big-image": "جانؽا ٱسلی",
+       "show-big-image-preview": "ٱندازٱ اؽ پیش ساٛلٛ:$1.",
+       "show-big-image-other": "Ù\87Ù\86Û\8c{{PLURAL:$2|گٱپ Ù\86Ù\85ا Ú©Ø±Ø¯Ù\86|Ú¯Û\89Ù¾ Ù\86Ù\85ا Ú©Ø±Ø¯Ù\86ؽا}}: $1.",
        "show-big-image-size": "$1 × $2 پیکسل",
        "file-info-gif-looped": "حلقه دار",
        "file-info-gif-frames": "$1 {{PLURAL:$1|فریم|فریمیا}}",
        "sunday-at": "یه شمه د $1",
        "yesterday-at": "دیرو د $1",
        "bad_image_list": "دونسمنديانه وه ای شلگ وارد بكيت:\n\nفقط سرخط يایی که وا * شرو بوئن د وير گرته بوئن. اولی چسب ون مئن هر سرخط، باید چسب ونی وه یک عسگ گن با.\nچسب ونيا نيایی د همو سرخط، وه عنوان چيا استثنادار د وير گرته بوئن",
-       "metadata": "رسÛ\8cÙ\86Ù±Û\8cا Ù\81اÙ\9bرٱ Ú¯Ù±Ù¾",
+       "metadata": "رسینٱیا فرٱ گٱپ",
        "metadata-help": "ای فایل شومل دونسمنیا هنی یه.شایت د دیربین رقم ون یا اسکنری که سی راس کردنشو استفاده بیه،وه ایچه اضاف بیه",
        "metadata-expand": "نشو دئن جزییات دمادیسگری",
        "metadata-collapse": "قام کردن جزییات دمادیسگری",
-       "metadata-fields": "رشنه یا یا گپ دونسمنیا که د ای پیغوم نومگه کاری بینه د ور گرته بلگه عسگ ن که گات وختی که جدول گپ دونسمنیا واز بوئه نشون دئیه بوئن.\nچی یا هنی سی یه که پیش فرضن قام بوئن.\n*راست کو\n*مدل\n*دم وخت اصل\n*وخت آشگار\n*اف ان شماره\n*ایزو نرخ من سرعت\n*فوکالنس\n*هنرمن\n*کپی رایت\n*حالت جی پی اس \n*جی پی اس گپ حالت\n*جی پی اس همه حالت",
+       "metadata-fields": "رشتٱیا یا گٱپ دونسمنیا کاْ د اؽ پاٛغوم نومگٱ کاری بینٱ د ڤٱر گرتٱ بٱلگٱ عٱسک کاْ ڤٱختؽ جٱدڤٱل گٱپ دونسمنیا ڤاز مۊئٱ نشوݩ داٛیٱ بۊئٱن.\nچی یا هنی سی یٱ کا پیش فٱرزٱن قایم مۊئٱن.\n*راس کو\n*مودل\n*دم ڤٱخت ٱسل\n*ڤٱخت آشگار\n*اْف اْن شمارٱ\n*ایزو نرخ من سرعت\n*فوکالنس\n*هونٱرمٱن\n*کوپی رایت\n*هالٱت جی پی اْس \n*جی پی اْس گٱپ هالٱت\n*جی پی اْس هٱمٱ هالٱت",
        "metadata-langitem": "<strong>$2:</strong> $1",
        "metadata-langitem-default": "$1",
-       "namespacesall": "Ù\87Ù±Ù\85ٱشÛ\8a",
+       "namespacesall": "Ù\87Ù\85ٱشÙ\88",
        "monthsall": "هٱمٱ",
        "confirmemail": "پشت راس کردن تیرنشون انجومانامه",
        "confirmemail_noemail": "شما د بلگه [[Special:Preferences|ترجیحات کاریاری]] خوتو یه گل تیرنشون انجومانامه نامعتور نه دئیته.",
        "version-libraries": "کتاو حونه یا پورسه بیه",
        "version-libraries-library": "کتاوگه",
        "version-libraries-version": "نسقه",
-       "redirect": "واگردونی وا جانیا،بلگه یا وانیئری نوم دیارکو",
-       "redirect-summary": "ای بلگه ویجه وا جانیا (نوم جانیا هیئش)، بلگه (شماره شناسیار بلگه یا شماره نسقه دیار بیه) یا بلگه کاریاری (شناسیار عددی کاریاری دیار بیه) واگردونی بوئه. طرز وه کار گرتن: [[{{#Special:Redirect}}/file/Example.jpg]]، \n[[{{#Special:Redirect}}/page/64308]]، [[{{#Special:Redirect}}/revision/328429]] یا [[{{#Special:Redirect}}/user/101]].",
+       "redirect": "ڤاگردونی ڤا جانؽا،بٱلگٱیا ڤانیری نوم دؽارکوݩ",
+       "redirect-summary": "اؽ بٱلگٱ ڤیژٱ ڤا جانؽا (نوم جانؽا هؽسش)، بٱلگٱ (شمارٱ شناسیار بٱلگٱ یا شمارٱ نۏسخٱ دؽار بیٱ) یا بٱلگٱ کاریاری (شناسؽار عدٱدی کاریاری دؽار بیٱ) ڤاگٱردونی بۊئٱ. تٱرز ڤ کار گرتن: [[{{#Special:Redirect}}/file/Example.jpg]]، \n[[{{#Special:Redirect}}/page/64308]]، [[{{#Special:Redirect}}/revision/328429]] یا [[{{#Special:Redirect}}/user/101]].",
        "redirect-submit": "رو",
        "redirect-lookup": "پی جوری:",
        "redirect-value": "ارزایشت:",
        "fileduplicatesearch-result-1": "جانیا «$1» تکراری نیئش.",
        "fileduplicatesearch-result-n": "جانیا «$1» {{PLURAL:$2|یه گل چی تکراری|$2 یه گل چی تکراری}} داره.",
        "fileduplicatesearch-noresults": "جانیایی وا نوم «$1» یافت نبی.",
-       "specialpages": "بٱلگٱیا ڤیجٱ",
+       "specialpages": "بٱلگٱیا ڤیژٱ",
        "specialpages-note-top": "میراث",
        "specialpages-note-restricted": "* بلگه یا ویجه عادی.\n* <span class=\"mw-specialpagerestricted\">بلگه یا ویجه محدود کاری بیه.</span>",
        "specialpages-group-maintenance": "گزارشتیا واداشتن",
        "intentionallyblankpage": "ای بلگه عمدن حالی هشته بیه",
        "external_image_whitelist": "یه خط نه ول بکید چی وه<pre>",
        "tags": "سردیسیا آلشت دئن خو",
-       "tag-filter": "فيلتر [[Special:سردیس|سردیسیا]]:",
+       "tag-filter": "فيلٛتر [[Special:سٱردیس|سٱردیسؽا]]:",
        "tag-filter-submit": "فيلتر",
-       "tag-list-wrapper": "[[Special:سردیسیا|{{PLURAL:$1|سردیس|سردیسیا}}]]: $2",
+       "tag-list-wrapper": "[[Special:سردیسؽا|{{PLURAL:$1|سردیس|سردیسؽا}}]]: $2",
        "tags-title": "سردیسیا",
        "tags-intro": "ای بلگه یه گل نومگه د سردیسیاییه که نرم افزار وا ونو ویرایشتیا نه نشو کاری میکه، الوت واگرد هومبراوریاشو.",
        "tags-tag": "نوم سردیس",
        "logentry-import-upload": "$1 $3 نه وه دس جانیا سوارکن {{GENDER:$2|وامین اورد}}",
        "logentry-import-interwiki": "$1 $3 نه د یه گل ویکی هنی {{GENDER:$2|وامین اورد}}",
        "logentry-merge-merge": "$1  $3  نه د $4 {{GENDER:$2| سریک سازی کرد}} (نسقه تا  $5)",
-       "logentry-move-move": "$1 {{GENDER:$2|جا ڤ جا کردناْ}} بٱلگٱ $3 ناْ سی $4",
+       "logentry-move-move": "$1 {{GENDER:$2|جا ڤ جا کردن}} بٱلگٱ $3 ناْ سی $4",
        "logentry-move-move-noredirect": "$1 بلگه $3 نه بی یه که یه گل واگردونی داشوئه د $4 {{GENDER:$2|جا وه جاکاری کرد}}",
        "logentry-move-move_redir": "$1 بلگه $3 نه د $4 که واگردونی بی {{GENDER:$2|جا وه جاکاری کرد}}",
        "logentry-move-move_redir-noredirect": "$1 بلگه $3 نه بی یه که یه گل واگردونی د $4 داشتوئه که یه خوش واگردونی بی {{GENDER:$2|جا وه جاکاری کرد}}",
        "logentry-patrol-patrol": "$1 نسقه $4 بلگه $3 نه چی یه گل چی تیه نئری بیه {{GENDER:$2|نشودار کرد}}",
        "logentry-patrol-patrol-auto": "$1 نسقه $4 بلگه $3 نه وه حال و بار خودانجوم چی یه گل بلگه تیه نیئر بیه {{GENDER:$2|نشودار کرد}}",
        "logentry-newusers-newusers": "حساو کاریاری $1 {{GENDER:$2|دروس بیه}}",
-       "logentry-newusers-create": "Ù\87ساÙ\88 Ú©Ø§Ø±Ø¨Ù±Ø±Û\8c $1 {{GENDER:$2|راس بی}}",
+       "logentry-newusers-create": "Ù\87ساÙ\88 Ú©Ø§Ø±Ø¨Ù±Ø±Û\8c $1 {{GENDER:$2|دÛ\8fرس بی}}",
        "logentry-newusers-create2": "حساو کاریاری $3،وه دس $1 {{GENDER:$2|دروس بی}}",
        "logentry-newusers-byemail": "حساو کاریاری $3 وه دس $1 {{GENDER:$2|ره وندیاری بی}} و رازینه گواردن وا انجومانام کل بی",
        "logentry-newusers-autocreate": "حساو $1  خودانجومن {{GENDER:$2|دروس بی}}",
index 31bf498..9d4130a 100644 (file)
        "noname": "Tu neesi norādījis derīgu lietotāja vārdu.",
        "loginsuccesstitle": "Pieteikšanās veiksmīga",
        "loginsuccess": "Tu esi ienācis {{grammar:lokatīvs|{{SITENAME}}}} kā \"$1\".",
-       "nosuchuser": "Šeit nav lietotāja ar vārdu \"$1\". Lietotājvārdi ir reģistrjutīgi (lielie un mazie burti nav viens un tas pats) Pārbaudi, vai pareizi uzrakstīts, vai arī [[Special:CreateAccount|izveido jaunu kontu]].",
+       "nosuchuser": "Nav lietotāja ar vārdu \"$1\".\nLietotājvārdi ir reģistrjutīgi.\nPārbaudi, vai pareizi uzrakstīts, vai arī [[Special:CreateAccount|izveido jaunu kontu]].",
        "nosuchusershort": "Šeit nav lietotāja ar vārdu \"$1\". Pārbaudi, vai nav drukas kļūda.",
        "nouserspecified": "Tev jānorāda lietotājvārds.",
        "login-userblocked": "Šis dalībnieks ir bloķēts. Pieslēgšanās nav atļauta.",
        "backend-fail-notexists": "Fails $1 nepastāv.",
        "backend-fail-hashes": "Neizdevās iegūt failu kontrolsummas salīdzināšanai.",
        "backend-fail-notsame": "Neidentisks fails jau pastāv $1.",
+       "backend-fail-invalidpath": "\"$1\" nav derīgs glabātuves ceļš.",
        "backend-fail-delete": "Nevar izdzēst failu $1.",
        "backend-fail-describe": "Nevarēja izmainīt faila \"$1\" metadatus.",
        "backend-fail-alreadyexists": "Fails $1 jau pastāv.",
index 599ec13..222821d 100644 (file)
        "botpasswords-label-grants": "अनुदान आवेदन:",
        "botpasswords-label-grants-column": "प्रदान कएल",
        "botpasswords-bad-appid": "बोट नाम \"$1\" मान्य नै अछि।",
+       "botpasswords-created-title": "बॉट पासवर्ड निर्मित बनल",
        "resetpass_forbidden": "कूटशब्द नै बदलल जा सकैए।",
        "resetpass_forbidden-reason": "कूटशब्द नै बदलल जा सकैए: $1",
        "resetpass-no-info": "अहाँकेँ ऐ पन्नाकेँ सोझे प्रयोग करबालेल सम्प्रवेशित हुअए पड़त।",
        "rcfilters-invalid-filter": "अमान्य फ़िल्टर",
        "rcfilters-filterlist-title": "चलनीसभ",
        "rcfilters-highlightmenu-title": "रंग चुनु",
+       "rcfilters-filterlist-noresults": "कोई फिल्टर नहीं भेटल",
        "rcfilters-filtergroup-authorship": "लेखक योगदान",
        "rcfilters-filter-editsbyself-label": "अहाक बदलावसभ",
        "rcfilters-filter-editsbyself-description": "अहाक अपन योगदान।",
        "rcfilters-filter-reviewstatus-auto-label": "सवापरिक्षित",
        "rcfilters-filtergroup-significance": "महत्व",
        "rcfilters-filter-minor-label": "छोट सम्पादन",
+       "rcfilters-filter-major-label": "गैर-मामूली संपादन",
        "rcfilters-filtergroup-watchlist": "देखेल पन्नासभ",
        "rcfilters-filter-watchlist-watched-label": "साकांक्षसूची",
        "rcfilters-filter-watchlist-watchednew-label": "नया ध्यानसूची बदलाव",
        "rcfilters-filter-watchlist-notwatched-label": "ध्यानसूची में नैछि",
+       "rcfilters-filtergroup-watchlistactivity": "ध्यानसूची क्रियाकलाप",
        "rcfilters-filter-watchlistactivity-unseen-label": "परिवर्तन सभ चुनु",
        "rcfilters-filter-watchlistactivity-seen-label": "परिवर्तन सभ चुनु",
        "rcfilters-filtergroup-changetype": "बदल क प्रकार:",
        "rcfilters-filter-lastrevision-label": "नूतन संशोधन",
        "rcfilters-filter-excluded": "अपवर्जित",
        "rcfilters-tag-prefix-namespace-inverted": " $1 <strong>:नैछि</strong>",
+       "rcfilters-exclude-button-off": "चयनित निकालु",
+       "rcfilters-exclude-button-on": "चयनित क छोड़कर",
        "rcfilters-view-tags": "पन्नाक संपादनसभ",
        "rcfilters-liveupdates-button": "अखुनका अद्यतन",
        "rcfilters-target-page-placeholder": "पृष्ठ(अथवा श्रेणी) क नाम भरू",
        "ipbreason-dropdown": "*सामान्य प्रतिबन्ध कारण\n** गलत सूचना घुसेनाइ\n** पन्ना सभसँ पाठ हटेनाइ\n** बाहरी जालस्थलक अवांछित लागि\n** फालतू/ अवांछित सामिग्रिक पान्नामे घुसाएब\n** धमकीबला व्यवहार/ तंग करब\n** कएक खाताकेँ गरिआएब\n** अवांछित प्रयोक्तानाम",
        "ipb-hardblock": "ऐ अनिकेत पतासँ सम्प्रवेशित प्रयोक्ताकेँ सम्पादनसँ रोकू",
        "ipbcreateaccount": "खाता निर्माण रोकू",
-       "ipbemailban": "पà¥\8dरयà¥\8bà¤\95à¥\8dताà¤\95à¥\87à¤\81 à¤\88-पतà¥\8dर à¤ªà¤ à¥\87बासà¤\81 à¤°à¥\8bà¤\95à¥\82",
+       "ipbemailban": "à¤\87मà¥\87ल à¤ªà¤ à¤¾à¤¬à¥\80",
        "ipbenableautoblock": "स्वचालित रूपेँ ऐ प्रयोक्ताक ऐ अनिकेतक प्रयोगकेँ प्रतिबन्धित करू, आ कोनो दोसर अनिकेत पताकेँ सेहो जतएसँ ओ सम्पादनक प्रयास करथि",
        "ipbsubmit": "ऐ प्रयोक्ताकेँ रोकू",
        "ipbother": "दोसर समए:",
        "ipb-confirm": "प्रतिबन्ध सुनिश्चित करू",
        "ipb-partial": "आंशिक",
        "ipb-pages-label": "पन्ना",
+       "ipb-namespaces-label": "नामस्थान",
        "badipaddress": "अमान्य आईपी पता",
        "blockipsuccesssub": "प्रतिबन्ध सफल भेल",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] प्रतिबन्धित कएल गेल।<br />\nदेखू [[Special:BlockList|IP block list]] प्रतिबन्धक पुनरीक्षण लेल।",
        "ipb-blocklist": "अखुनका प्रतिबंधित देखू",
        "ipb-blocklist-contribs": "$1 लेल अवदान",
        "ipb-blocklist-duration-left": "$1 बाकी",
+       "block-actions": "अवरोध कार्यवाही:",
        "block-expiry": "खतम हएत:",
+       "block-options": "अतिरिक्त विकल्प:",
+       "block-prevent-edit": "सम्पादन",
+       "block-reason": "कारण:",
+       "block-target": "सदस्यनाम या आईपी पता:",
        "unblockip": "प्रयोक्ताकेँ प्रतिबन्ध सँ हटाबी",
        "unblockiptext": "पहिनेसँ प्रतिबन्धित अनिकेत वा प्रयोक्तानामकेँ लिखबाक अधिकार देबा लेल निचुलका आवेदन भरू।",
        "ipusubmit": "ई  प्रतिबन्ध हटाउ",
        "emailblock": "ई-पत्र प्रतिबन्धित",
        "blocklist-nousertalk": "अपन वार्ता पन्ना सम्पादित नै कऽ सकब",
        "blocklist-editing": "सम्पादन कऽ रहल छी",
+       "blocklist-editing-page": "पृष्ठ",
+       "blocklist-editing-ns": "नामस्थान",
        "ipblocklist-empty": "प्रतिबन्धसूची खाली अछि।",
        "ipblocklist-no-results": "आग्रह कएल अनिकेत वा प्रयोक्तानाम प्रतिबन्धित नै कएल गेल।",
        "blocklink": "प्रतिबन्धित",
        "tags-edit-manage-link": "ट्याग व्यवस्थापन",
        "tags-edit-revision-selected": "[[:$2]] {{PLURAL:$1|क|के}} चयनित अवतरण:",
        "tags-edit-logentry-selected": "{{PLURAL:$1|चुनल लौग घटना|चुनल लौग घटनासभ}}:",
+       "tags-edit-existing-tags": "मौजूद टैग",
        "tags-edit-existing-tags-none": "<em>कोनो नै</em>",
        "tags-edit-new-tags": "नव ट्याग:",
        "tags-edit-add": "इ ट्यागसभ जोडी:",
        "logentry-newusers-create2": "$1 {{GENDER:$2|बनाएल}} {{GENDER:$4|एकटा प्रयोक्ता खाता}} $3",
        "logentry-newusers-byemail": "$1 द्वारा प्रयोक्ता खाता $3 {{GENDER:$2|बनाओल}} गेल आ कूटशब्द ई-पत्र द्वारा भेजल गेल",
        "logentry-newusers-autocreate": "खाता $1 छल {{GENDER:$2|बनाएल}} स्वतः",
+       "logentry-protect-protect": "$1 ने $3 $4 {{GENDER:$2|सुरक्षित}} किरल।",
        "logentry-upload-upload": "$1 {{GENDER:$2|ए}} $3 अपलोड केलक",
+       "log-name-managetags": "समय प्रबंधन लॉग",
        "logentry-managetags-create": "$1 {{GENDER:$2 बनाएल}} टैग $4",
        "log-name-tag": "ट्याग लौग",
        "rightsnone": "(कोनो नै)",
        "authmanager-email-help": "ई-पत्र ठेगान:",
        "authmanager-realname-label": "असली नाम",
        "authmanager-provider-temporarypassword": "तात्कालिक कूटशब्द:",
+       "authprovider-confirmlink-success-line": "$1 : सफलतापूर्वक जुड़ा।",
        "authprovider-resetpass-skip-label": "छाेड",
        "authform-newtoken": "टोकन नै अछि । $1",
        "authform-notoken": "टोकन नै अछि",
index 2415161..e2abf15 100644 (file)
        "previewerrortext": "Се појави грешка при обидот да се прегледаат промените.",
        "blockedtitle": "Корисникот е блокиран",
        "blocked-email-user": "<strong><strong>На вашето корисничко име му е забрането да праќа е-пошта. Можете сепак да уредувате други страници на ова вики.</strong> Сите поединости за забраната ќе ги најдете во [[Special:MyContributions|придонесите на сметката]].\n\nЗабраната ја дал $1.\n\nНаведената причина гласи <em>$2</em>.\n\n* Почеток на забраната: $8\n* Истек на забраната: $6\n* Предвиден забраненик: $7\n* Назнака на забраната #$5",
-       "blockedtext-partial": "<strong>На вашето корисничко или IP-адреса му е забрането да прави измени на страницава. Можете сепак да уредувате други страници на ова вики.</strong> Сите поединости за забраната ќе ги најдете во [[Special:MyContributions|придонесите на сметката]].\n\nЗабраната ја дал $1.\n\nНаведената причина гласи <em>$2</em>.\n\n* Почеток на забраната: $8\n* Истек на забраната: $6\n* Предвиден забраненик: $7\n* Назнака на забраната #$5",
+       "blockedtext-partial": "<strong>Ð\9dа Ð²Ð°Ñ\88еÑ\82о ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP-адÑ\80еÑ\81а Ð¼Ñ\83 Ðµ Ð·Ð°Ð±Ñ\80анеÑ\82о Ð´Ð° Ð¿Ñ\80ави Ð¸Ð·Ð¼ÐµÐ½Ð¸ Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86ава. Ð\9cожеÑ\82е Ñ\81епак Ð´Ð° Ñ\83Ñ\80едÑ\83ваÑ\82е Ð´Ñ\80Ñ\83ги Ñ\81Ñ\82Ñ\80аниÑ\86и Ð½Ð° Ð¾Ð²Ð° Ð²Ð¸ÐºÐ¸.</strong> Ð¡Ð¸Ñ\82е Ð¿Ð¾ÐµÐ´Ð¸Ð½Ð¾Ñ\81Ñ\82и Ð·Ð° Ð·Ð°Ð±Ñ\80анаÑ\82а Ñ\9cе Ð³Ð¸ Ð½Ð°Ñ\98деÑ\82е Ð²Ð¾ [[Special:MyContributions|пÑ\80идонеÑ\81иÑ\82е Ð½Ð° Ñ\81меÑ\82каÑ\82а]].\n\nÐ\97абÑ\80анаÑ\82а Ñ\98а Ð´Ð°Ð» $1.\n\nÐ\9dаведенаÑ\82а Ð¿Ñ\80иÑ\87ина Ð³Ð»Ð°Ñ\81и <em>$2</em>.\n\n* Ð\9fоÑ\87еÑ\82ок Ð½Ð° Ð·Ð°Ð±Ñ\80анаÑ\82а: $8\n* Ð\98Ñ\81Ñ\82ек Ð½Ð° Ð·Ð°Ð±Ñ\80анаÑ\82а: $6\n* Ð\9fÑ\80едвиден Ð·Ð°Ð±Ñ\80аненик: $7\n* Ð\9dазнака Ð½Ð° Ð·Ð°Ð±Ñ\80анаÑ\82а #$5",
        "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": "Вашата 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 назнака на блокирањетo е $5.\nВе молиме наведете ги овие подробности доколку реагирате на блокирањето.",
        "systemblockedtext": "Вашето корисничко име или IP-адреса е автоматски блокирано од МедијаВики.\nПонудена причина:\n\n:<em>$2</em>\n\n* Почеток на блокот: $8\n* Истек на блокот: $6\n* Блокот е наменет за: $7\n\nВашата тековна IP-адреса гласи $3.\nПрепишете ги сите горенаведени поединости доколку сакате да се распрашате кај надлежните во врска со блокот.",
        "history-feed-description": "Историја на измените на оваа страница на викито",
        "history-feed-item-nocomment": "$1 на $2",
        "history-feed-empty": "Бараната страница не постои.\nМоже била избришана од викито или преименувана.\nОбидете се да [[Special:Search|пребарате низ викито]] за релевантни нови страници.",
-       "history-edit-tags": "Ð\98змени Ð¾Ð·Ð½Ð°ÐºÐ¸ Ð´а одредени преработки",
+       "history-edit-tags": "Ð\98змени Ð¾Ð·Ð½Ð°ÐºÐ¸ Ð½а одредени преработки",
        "rev-deleted-comment": "(избришан опис на промени)",
        "rev-deleted-user": "(избришано корисничко име)",
        "rev-deleted-event": "(избришани податоци од дневникот)",
        "ipb_expiry_old": "Времето на истекување е постаро од тековното време.",
        "ipb_expiry_temp": "Скриените блокирања на корисникот мора да бидат перманентни.",
        "ipb_hide_invalid": "Оваа сметка не може да се потисне; има {{PLURAL:$1|повеќе од едно уредување|преку $1 уредувања}}..",
+       "ipb_hide_partial": "Скриените забрани за кориснички имиња мора да важат за цело вики.",
        "ipb_already_blocked": "„$1“ е веќе блокиран",
        "ipb-needreblock": "$1 е веќе блокиран. Дали сакате да направите промена?",
        "ipb-otherblocks-header": "{{PLURAL:$1|Друго блокирање|Други блокирања}}",
        "cant-move-subpages": "Немате дозвола за преместување на потстраници.",
        "namespace-nosubpages": "Именскиот простор „$1“ не дозволува потстраници.",
        "newtitle": "Нов наслов:",
-       "move-watch": "Набљудувај ја страницава",
+       "move-watch": "Набљудувај ги изворната и целната страници",
        "movepagebtn": "Премести страница",
        "pagemovedsub": "Успешно преместување",
        "cannotmove": "Страницата не може да се премести од {{PLURAL:$1|следнава причина|следниве причини}}:",
index a78b0e5..41dd0cc 100644 (file)
        "uploadnewversion-linktext": "Ынкаркэ о версиуне ноуэ а ачестуй фишиер",
        "randompage": "Артикол ынтымплэтор",
        "statistics": "Статистикэ",
+       "statistics-header-pages": "Статистика паӂинилор",
+       "statistics-header-edits": "Статистика модификэрилор",
+       "statistics-header-users": "Статистика утилизаторилор",
+       "statistics-header-hooks": "Алте статистичь",
+       "statistics-articles": "Артиколе",
+       "statistics-pages": "Паӂинь",
+       "statistics-pages-desc": "Тоате паӂиниле дин вики, инклусив паӂиниле де дискуций, редирекционэрь ш.а.",
+       "statistics-files": "Фишиере ынкэркате",
+       "statistics-edits": "Нумэрул де модификэрь дин моментул инсталэрий проектулуй {{SITENAME}}",
+       "statistics-edits-average": "Нумэрул медиу де модификэрь пе паӂинэ",
        "nbytes": "{{PLURAL:$1|ун байт|$1 байць|$1 де байць}}",
        "nmembers": "$1 {{PLURAL:$1|ун мембру|мембрь}}",
        "prefixindex": "Тоате паӂиниле ку префикс",
index 99af87d..a98909e 100644 (file)
        "createacct-another-submit": "खाता खोल्नुहोस्",
        "createacct-continue-submit": "खाता निर्माणलाई निरन्तरता दिनुहोस्",
        "createacct-another-continue-submit": "खाता निर्माणलाई निरन्तरता दिनुहोस्",
-       "createacct-benefit-heading": "{{SITENAME}} तपाईं जस्तै मानिसहरूद्वारा सिर्जना गरिएको हो ।",
+       "createacct-benefit-heading": "{{SITENAME}} तपाई जस्तै मानिसहरूद्वारा सिर्जना गरिएको हो।",
        "createacct-benefit-body1": "{{PLURAL:$1|सम्पादन|सम्पादनहरू}}",
        "createacct-benefit-body2": "{{PLURAL:$1|पृष्ठ|पृष्ठहरू}}",
        "createacct-benefit-body3": "हालैका {{PLURAL:$1|योगदानकर्ता|योगदानकर्ताहरू}}",
index c83dfe6..71a148d 100644 (file)
        "tool-link-userrights-readonly": "{{GENDER:$1|Gebruikers}}groepen weergeven",
        "tool-link-emailuser": "Deze {{GENDER:$1|gebruiker}} e-mailen",
        "imagepage": "Bestandspagina bekijken",
-       "mediawikipage": "Berichtpagina bekijken",
+       "mediawikipage": "Berichtenpagina bekijken",
        "templatepage": "Sjabloonpagina bekijken",
        "viewhelppage": "Hulppagina bekijken",
        "categorypage": "Categoriepagina bekijken",
        "uctop": "laatste wijziging",
        "month": "Van maand (en eerder):",
        "year": "Van jaar (en eerder):",
-       "date": "Vanaf (en eerder):",
+       "date": "Op deze datum (en eerder):",
        "sp-contributions-newbies": "Alleen bijdragen van nieuwe accounts bekijken",
        "sp-contributions-newbies-sub": "Voor nieuwelingen",
        "sp-contributions-newbies-title": "Gebruikersbijdragen van nieuwe accounts",
        "ipb_expiry_old": "Vervaldatum is in het verleden.",
        "ipb_expiry_temp": "Blokkades voor verborgen gebruikers moeten permanent zijn.",
        "ipb_hide_invalid": "Het is niet mogelijk dit account te verbergen; het heeft meer dan {{PLURAL:$1|één bewerking|$1 bewerkingen}}.",
+       "ipb_hide_partial": "Blokkades die de gebruikersnaam verbergen moeten op de gehele wiki van toepassing zijn.",
        "ipb_already_blocked": "\"$1\" is al geblokkeerd",
        "ipb-needreblock": "$1 is al geblokkeerd.\nWilt u de instellingen wijzigen?",
        "ipb-otherblocks-header": "Andere {{PLURAL:$1|blokkade|blokkades}}",
index 05be43b..3e9139e 100644 (file)
        "ipb_expiry_old": "O tempo de expiração está no passado.",
        "ipb_expiry_temp": "Bloqueios com nome de usuário ocultado devem ser permanentes.",
        "ipb_hide_invalid": "Não foi possível suprimir esta conta; ela tem mais de {{PLURAL:$1|uma}}edições.",
+       "ipb_hide_partial": "Bloqueios de nome de usuário ocultos devem ser bloqueados em todo o site.",
        "ipb_already_blocked": "\"$1\" já se encontra bloqueado",
        "ipb-needreblock": "$1 já se encontra bloqueado. Deseja alterar as configurações?",
        "ipb-otherblocks-header": "{{PLURAL:$1|Outro bloqueio|Outros bloqueios}}",
        "logentry-block-block": "$1 {{GENDER:$2|bloqueou}} {{GENDER:$4|$3}} com um tempo de expiração de $5 $6",
        "logentry-block-unblock": "$1 {{GENDER:$2|desbloqueou}} {{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1 {{GENDER:$2|modificou}} configurações de bloqueio para {{GENDER:$4|$3}} com um tempo de expiração de $5 $6",
-       "logentry-partialblock-block": "$1 {{GENDER:$2|aplicou a}} {{GENDER:$4|$3}} um bloqueio que impede a edição {{PLURAL:$8|da página|das páginas}} $7 com um prazo de expiração de $5 $6",
-       "logentry-partialblock-reblock": "$1 {{GENDER:$2|alterou}} as definições do bloqueio de {{GENDER:$4|$3}} para impedir a edição {{PLURAL:$8|da página|das páginas}} $7 com um prazo de expiração de $5 $6",
+       "logentry-partialblock-block-page": "{{PLURAL:$1|da página|das páginas}} $2",
+       "logentry-partialblock-block-ns": "{{PLURAL:$1|do espaço nominal|dos espaços nominais}} $2",
+       "logentry-partialblock-block": "$1 {{GENDER:$2|aplicou a}} {{GENDER:$4|$3}} um bloqueio que impede a edição $7 com um prazo de expiração de $5 $6",
+       "logentry-partialblock-reblock": "$1 {{GENDER:$2|alterou}} as definições do bloqueio de {{GENDER:$4|$3}} para impedir a edição $7 com um prazo de expiração de $5 $6",
        "logentry-non-editing-block-block": "$1 {{GENDER:$2|aplicou a}} {{GENDER:$4|$3}} um bloqueio que impede as operações que não sejam edições especificada , com um prazo de expiração de $5 $6",
        "logentry-non-editing-block-reblock": "$1 {{GENDER:$2|alterou}} as definições do bloqueio de {{GENDER:$4|$3}} para impedir as operações que não sejam edições especificada, com um prazo de expiração de $5 $6",
        "logentry-suppress-block": "$1 {{GENDER:$2|bloqueou}} {{GENDER:$4|$3}} com um tempo de expiração de $5 $6",
index db793e0..fa9bf44 100644 (file)
        "range_block_disabled": "Used as error message in [[Special:Block]].\n\nSee also:\n* {{msg-mw|Range block disabled}}\n* {{msg-mw|Ip range invalid}}\n* {{msg-mw|Ip range toolarge}}",
        "ipb_expiry_invalid": "Used as error message in [[Special:Block]].",
        "ipb_expiry_old": "Used as error message in [[Special:Block]], if the expiry time is in the past.\n{{Identical|protect_expiry_old}}",
-       "ipb_expiry_temp": "Warning message displayed on [[Special:BlockIP]] if the option \"hide username\" is selected but the expiry time is not infinite.",
+       "ipb_expiry_temp": "Warning message displayed on [[Special:Block]] if the option \"hide username\" is selected but the expiry time is not infinite.",
        "ipb_hide_invalid": "Used as error message in [[Special:Block]].\n* $1 - Number of edits (Value of [[mw:Manual:$wgHideUserContribLimit]])",
+       "ipb_hide_partial": "Warning message displayed on [[Special:Block]] if the option \"hide username\" is selected but the block is a partial block.",
        "ipb_already_blocked": "{{Identical|$1 is already blocked}}",
        "ipb-needreblock": "Used in [[Special:Block]].\n* $1 - target username, can be used for GENDER support",
        "ipb-otherblocks-header": "[[File:Special.Block with other blocks from GlobalBlocking and TorBlocks.png|thumb|Example]]\nUsed on [[Special:Block]] as header for other blocks, i.e. from GlobalBlocking or TorBlocks\n\nParameters:\n* $1 - number of blocks\nSee also:\n* {{msg-mw|Ipblocklist-otherblocks}}",
index 2567eb8..66f3774 100644 (file)
        "rcfilters-empty-filter": "Nisciune filtre attive. Tutte le condrebbute avènene fatte 'ndrucà.",
        "rcfilters-filterlist-title": "Filtre",
        "rcfilters-filterlist-whatsthis": "Cumme funzionane?",
+       "rcfilters-filterlist-feedbacklink": "Dì ce ne pinze de ste struminde de filtre",
        "rcfilters-highlightbutton-title": "Evidenzie le resultate",
        "rcfilters-highlightmenu-title": "Scacchie 'nu culore",
        "rcfilters-highlightmenu-help": "Scacchie 'nu culore pe evidenzià sta probbietà",
        "rcfilters-filterlist-noresults": "Nisciune filtre acchiate",
+       "rcfilters-noresults-conflict": "Nisciune resultate acchiate, purcé le criterie de recerche stonne in conflitte",
        "rcfilters-filter-editsbyself-label": "Cangiaminde tune",
        "rcfilters-filter-editsbyself-description": "Condrebbute tune.",
        "rcfilters-filter-editsbyother-label": "Cangiaminde de l'otre",
index 600436e..fb9452c 100644 (file)
        "createacct-email-ph": "Введите свой адрес электронной почты",
        "createacct-another-email-ph": "Введите адрес электронной почты",
        "createaccountmail": "Использовать сгенерированный случайным образом временный пароль и выслать его на указанный адрес электронной почты",
-       "createaccountmail-help": "\nМожет использоваться, чтобы создать учетную запись для другого лица, не узнавая пароль.",
+       "createaccountmail-help": "Может использоваться, чтобы создать учетную запись для другого лица, не узнавая пароль.",
        "createacct-realname": "Настоящее имя (необязательно)",
        "createacct-reason": "Причина",
        "createacct-reason-ph": "Зачем вы создаёте другую учётную запись",
        "previewerrortext": "При попытке отобразить предварительный просмотр ваших изменений произошла ошибка.",
        "blockedtitle": "Участник заблокирован",
        "blocked-email-user": "<strong>Для вашей учётной записи заблокирована отправка электронной почты. Но вы по-прежнему можете редактировать страницы в этом вики-проекте.</strong> Вы можете увидеть все подробности блокировки на странице [[Special:MyContributions|вклада учётной записи]].\n\nБлокировка произведена администратором $1.\n\nУказана следующая причина: <em>$2</em>.\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n* Идентификатор блокировки: #$5",
-       "blockedtext-partial": "<strong>Для вашей учётной записи заблокирована возможность изменения этой страницы. Но вы по-прежнему можете редактировать другие страницы в этом вики-проекте.</strong> Вы можете увидеть все подробности блокировки на странице [[Special:MyContributions|вклада учётной записи]].\n\nБлокировка произведена администратором $1.\n\nУказана следующая причина: <em>$2</em>.\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n* Идентификатор блокировки: #$5",
+       "blockedtext-partial": "<strong>Ð\94лÑ\8f Ð²Ð°Ñ\88ей Ñ\83Ñ\87Ñ\91Ñ\82ной Ð·Ð°Ð¿Ð¸Ñ\81и Ð¸Ð»Ð¸ IP-адÑ\80еÑ\81а Ð·Ð°Ð±Ð»Ð¾ÐºÐ¸Ñ\80ована Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñ\81Ñ\82Ñ\8c Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ\8f Ñ\8dÑ\82ой Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b. Ð\9dо Ð²Ñ\8b Ð¿Ð¾-пÑ\80ежнемÑ\83 Ð¼Ð¾Ð¶ÐµÑ\82е Ñ\80едакÑ\82иÑ\80оваÑ\82Ñ\8c Ð´Ñ\80Ñ\83гие Ñ\81Ñ\82Ñ\80аниÑ\86Ñ\8b Ð² Ñ\8dÑ\82ом Ð²Ð¸ÐºÐ¸-пÑ\80оекÑ\82е.</strong> Ð\92Ñ\8b Ð¼Ð¾Ð¶ÐµÑ\82е Ñ\83видеÑ\82Ñ\8c Ð²Ñ\81е Ð¿Ð¾Ð´Ñ\80обноÑ\81Ñ\82и Ð±Ð»Ð¾ÐºÐ¸Ñ\80овки Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86е [[Special:MyContributions|вклада Ñ\83Ñ\87Ñ\91Ñ\82ной Ð·Ð°Ð¿Ð¸Ñ\81и]].\n\nÐ\91локиÑ\80овка Ð¿Ñ\80оизведена Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñ\81Ñ\82Ñ\80аÑ\82оÑ\80ом $1.\n\nУказана Ñ\81ледÑ\83Ñ\8eÑ\89аÑ\8f Ð¿Ñ\80иÑ\87ина: <em>$2</em>.\n\n* Ð\9dаÑ\87ало Ð±Ð»Ð¾ÐºÐ¸Ñ\80овки: $8\n* Ð\9eконÑ\87ание Ð±Ð»Ð¾ÐºÐ¸Ñ\80овки: $6\n* Ð¦ÐµÐ»Ñ\8c Ð±Ð»Ð¾ÐºÐ¸Ñ\80овки: $7\n* Ð\98денÑ\82иÑ\84икаÑ\82оÑ\80 Ð±Ð»Ð¾ÐºÐ¸Ñ\80овки: #$5",
        "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": "Ваш IP-адрес автоматически заблокирован в связи с тем, что он ранее использовался кем-то из участников, заблокированных администратором $1. \nБыла указана следующая причина блокировки:\n\n: «$2».\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n\nВы можете связаться с $1 или любым другим [[{{MediaWiki:Grouppage-sysop}}|администратором]], чтобы обсудить блокировку.\n\nОбратите внимание, что вы не сможете использовать функцию «{{int:emailuser}}», если в своих [[Special:Preferences|персональных настройках]] не задали или не подтвердили корректный адрес электронной почты, или если ваша блокировка включает запрет отправки писем подобным образом.\n\nВаш IP-адрес — $3, идентификатор блокировки — #$5.\nПожалуйста, указывайте эти сведения в любых своих обращениях.",
        "systemblockedtext": "Ваше имя участника или IP-адрес были автоматически заблокированы MediaWiki.\nУказана следующая причина:\n\n:<em>$2</em>\n\n* Начало блокировки: $8\n* Окончание блокировки: $6\n* Цель блокировки: $7\n\nВаш текущий IP-адрес $3.\nПожалуйста, указывайте все эти сведения в любых своих обращениях.",
        "userrights-expiry": "Права истекают:",
        "userrights-expiry-existing": "$2, $3",
        "userrights-expiry-othertime": "Другое время:",
-       "userrights-expiry-options": "1 день:1 day,1 неделя:1 week,1 месяц:1 mopnth,3 месяца:3 months,6 месяцев:6 months,1 год:1 year",
+       "userrights-expiry-options": "1 день:1 day,1 неделя:1 week,1 месяц:1 month,3 месяца:3 months,6 месяцев:6 months,1 год:1 year",
        "userrights-invalid-expiry": "Время истечения для группы «$1» задано неверно.",
        "userrights-expiry-in-past": "Время истечения для группы «$1» задано в прошлом.",
        "userrights-cannot-shorten-expiry": "Вы не можете перенести на более ранний срок дату истечения членства в группе «$1». Только участники, имеющие право на добавление и удаление этой группы, могут перенести её на более ранний срок.",
        "ipb_expiry_old": "Время окончания — в прошлом.",
        "ipb_expiry_temp": "Блокировки с сокрытием имени участника должны быть бессрочными.",
        "ipb_hide_invalid": "Невозможно скрыть эту учётную запись, с неё сделано более {{PLURAL:$1|одной правки|$1 правок}}.",
+       "ipb_hide_partial": "Скрытые запреты имён участников должны действовать во всей вики.",
        "ipb_already_blocked": "«$1» уже заблокирован.",
        "ipb-needreblock": "$1 уже {{GENDER:$1|заблокирован|заблокирована}}. Хотите изменить параметры блокировки?",
        "ipb-otherblocks-header": "{{PLURAL:$1|1=Другая блокировка|Другие блокировки}}",
        "movepagetalktext": "Если вы отметите эту галочку, соответствующая страница обсуждения будет также автоматически переименована, если только уже не существует непустая страница обсуждения с таким же названием.\n\nВ этом случае вам нужно будет переименовать или объединить страницы вручную, если это необходимо.",
        "moveuserpage-warning": "<strong>Внимание:</strong> вы собираетесь переименовать страницу участника. Пожалуйста, обратите внимание, что переименована будет только страница, участник <strong>не</strong> будет переименован.",
        "movecategorypage-warning": "<strong>Предупреждение:</strong> Вы собираетесь переименовать страницу категории. Пожалуйста, обратите внимание, что будет переименована только эта страница, а все страницы старой категории <em>не</em> будут перекатегоризованы в новую.",
-       "movenologintext": "Вы должны [[Special:UserLogin|представиться системе]],\nчтобы иметь возможность переименовать страницы.",
+       "movenologintext": "Вы должны [[Special:UserLogin|представиться системе]], чтобы иметь возможность переименовать страницы.",
        "movenotallowed": "У вас нет прав на переименовывание страниц.",
        "movenotallowedfile": "У вас нет прав на переименовывание файлов.",
        "cant-move-user-page": "У вас нет прав на переименовывание основных страниц участников.",
index f51d68f..d5e5473 100644 (file)
        "tog-watchdefault": "Придавати мнов едітованы сторінкы і файлы до списку слїдованых",
        "tog-watchmoves": "Придавати переменованы сторінкы і файлы до мого списку слїдованых",
        "tog-watchdeletion": "Придавати сторінкы і файлы, котры змажу, міджі слїдованы",
+       "tog-watchuploads": "Придавати мнов наладованы файлы до списку слїдованых",
        "tog-minordefault": "Імпліцітно позначіти вшыткы зміны як малы",
-       "tog-previewontop": "Вказовати нагляд перед окном едітованя  (не за ним)",
+       "tog-previewontop": "Вказовати нагляд перед окном едітованя",
        "tog-previewonfirst": "Вказати нагляд при першій едітації",
        "tog-enotifwatchlistpages": "Послати електронічну пошту, кідь ся змінила сторінка або файл з мого списку слїдованя",
        "tog-enotifusertalkpages": "Послати електронічну пошту при змінї моёй діскузной сторінкы",
        "tog-enotifminoredits": "Послати електронічну пошту і про меншы едітованя сторінок і файлів",
        "tog-enotifrevealaddr": "Прозрадити мою поштову адресу в поштї  увідомлїня",
        "tog-shownumberswatching": "Вказати кілько хоснователїв придало сторінку до свого списку слїдованых",
-       "tog-oldsig": "Ð\86снуючій підпис:",
+       "tog-oldsig": "Ð\92аÑ\88 Ñ\96снуючій підпис:",
        "tog-fancysig": "Хосновати про підпис вікітекст (без автоматічного одказу)",
        "tog-uselivepreview": "Хосновати швыдкый нагляд (експеріментално)",
        "tog-forceeditsummary": "Упозорнити ня, кідь не є выповнене згорнутя едітованя",
        "morenotlisted": "Гевтот список неповный ...",
        "mypage": "Сторінка",
        "mytalk": "Діскузія",
-       "anontalk": "Діскузія к тїй IP-адресї",
+       "anontalk": "Діскузія",
        "navigation": "Навіґація",
        "and": "&#32;і",
        "faq": "Часты звідованя",
        "searcharticle": "Перейти",
        "history": "Історія сторінкы",
        "history_short": "Історія",
+       "history_small": "історія",
        "updatedmarker": "обновлено од послїднёй навщівы",
        "printableversion": "Верзія до друку",
        "permalink": "Перманентный одказ",
        "print": "Друк",
        "view": "Видіти",
+       "view-foreign": "Видіти на $1",
        "edit": "Едітовати",
        "create": "Створити",
        "delete": "Вымазати",
        "talk": "Діскузія",
        "views": "Перегляды",
        "toolbox": "Інштрументы",
+       "tool-link-userrights": "Змінити {{GENDER:$1|хосновательскы}} групы",
+       "tool-link-userrights-readonly": "Перегляд {{GENDER:$1|хосновательскых}} груп",
+       "tool-link-emailuser": "Загнати імейл {{GENDER:$1|тому хоснователёви|тій хосновательцї}}",
        "imagepage": "Посмотрити сторінку файлу",
        "mediawikipage": "Посмотрити сторінку повідомлїнь",
        "templatepage": "Посмотрити шаблону",
        "disclaimers": "Вылучіня одповідности",
        "disclaimerpage": "Project:Відмова від відповідальності",
        "edithelp": "Поміч едітованя",
+       "helppage-top-gethelp": "Поміч",
        "mainpage": "Головна сторінка",
        "mainpage-description": "Головна сторінка",
        "policy-url": "Project:Правила",
        "actionthrottled": "Акція была придушена",
        "actionthrottledtext": "Взглядом ку протиспамовым крокам не можете жадану акцію провести барз часто в короткім часї. Спробуйте то знову о пару мінут.",
        "protectedpagetext": "Тота сторінка была замкнута, жебы не годен быв єй едітовати або інакше мінити.",
-       "viewsourcetext": "Можете видїти і копіровати код той сторінкы:",
-       "viewyourtext": "Можете собі посмотрити і скопіровати жрідловый текст '''вашых змін''' той сторінкы:",
+       "viewsourcetext": "Можете видїти і копіровати код той сторінкы.",
+       "viewyourtext": "Можете собі посмотрити і скопіровати жрідловый текст <strong>вашых змін</strong> той сторінкы.",
        "protectedinterface": "Тота сторінка є частёв інтрефейсу проґрамового забеспечіня той вікі і єй можуть едітовати лем адміністраторы проєкту.\nЖебы придати або змінити переклады, просиме хоснуйте [https://translatewiki.net/ translatewiki.net], локалізачный проєкт MediaWiki.",
-       "editinginterface": "'''Позірь:''' Едітуєте сторінку,котра є частинов текстового інтерфейсу.\nЗміны той сторінкы выкличуть зміну інтерфейсу про іншых хоснователїв той вікі. \nДодати ці змінити переклады на вшыткых вікі просиме хоснуйте [https://translatewiki.net/wiki/Main_Page?setlang=uk translatewiki.net] — проєкт, што ся занимать локалізаціов MediaWiki.",
+       "editinginterface": "<strong>Позірь:</strong> Едітуєте сторінку, котра є частинов текстового інтерфейсу.\nЗміны той сторінкы выкличуть зміну інтерфейсу про іншых хоснователїв той вікі.",
+       "translateinterface": "Додати ці змінити переклады на вшыткых вікі просиме хоснуйте [https://translatewiki.net/ translatewiki.net] — проєкт, што ся занимать локалізаціов MediaWiki.",
        "cascadeprotected": "Сторінка є замнкута, бо є вложена до  {{PLURAL:$1|наслїдуючой сторінкы замкнуты|наслїдуючіх сторінок замнкнутых|наслїдуючіх сторінок замнкнутых}} каскадовым замком:\n$2",
        "namespaceprotected": "Не маєте права едітовати сторінкы в просторї  назв «$1».",
        "customcssprotected": "Не маєте права едітовати тоту сторінку з CSS, бо обсягує персоналны наставлїна іншого хоснователя.",
        "invalidtitle-knownnamespace": "Непряавилна назва в просторї назв „$2“ і текстом „$3“",
        "invalidtitle-unknownnamespace": "Неправилна назва з незнамым чіслом простору назв $1 і текстом „$2“",
        "exception-nologin": "Не сьте приголошеный(а)",
-       "exception-nologin-text": "Ð\96ебÑ\8b Ñ\81Ñ\8f Ð´Ñ\96Ñ\81Ñ\82аÑ\82и Ð½Ð¾ Ñ\82оÑ\82Ñ\83 Ñ\81Ñ\82оÑ\80Ñ\96нкÑ\83 Ñ\81Ñ\8f Ð¿Ñ\80оÑ\81име [[Special:Userlogin|пÑ\80иголоÑ\81Ñ\8cÑ\82е]].",
+       "exception-nologin-text": "Ð\96ебÑ\8b Ñ\81Ñ\8f Ð´Ñ\96Ñ\81Ñ\82аÑ\82и Ð½Ð° Ñ\82оÑ\82Ñ\83 Ñ\81Ñ\82оÑ\80Ñ\96нкÑ\83 Ñ\81Ñ\8f Ð¿Ñ\80оÑ\81име Ð¿Ñ\80иголоÑ\81Ñ\8cÑ\82е.",
        "exception-nologin-text-manual": "Жебы ся дістати на тоту сторінку ся мусите $1.",
        "virus-badscanner": "Зла конфіґурація: незнамый антивіровый проґрам: ''$1''",
        "virus-scanfailed": "скенованя ся не вдало (код $1)",
        "createacct-reason": "Прічіна",
        "createacct-reason-ph": "Чом собі робите друге конто",
        "createacct-submit": "Створити конто",
-       "createacct-another-submit": "Створити інше конто",
+       "createacct-another-submit": "Створити конто",
        "createacct-benefit-heading": "{{grammar:4sg|{{SITENAME}}}} творять люде як вы.",
        "createacct-benefit-body1": "{{PLURAL:$1|едітованя|едітованя|едітовань}}",
        "createacct-benefit-body2": "{{PLURAL:$1|сторінка|сторінкы|сторінок}}",
        "noname": "Мусите увести мено свого конта.",
        "loginsuccesstitle": "Успішне приголошіня",
        "loginsuccess": "'''Теперь працуєте {{grammar:locative|{{SITENAME}}}} під меном $1.'''",
-       "nosuchuser": "Не екзістує хоснователь з меном «$1». У хосновательскых мен ся розлишують малы/великы писмена. Сконтролюйте запис, або собі [[Special:CreateAccount|зареґіструйте нове конто]].",
+       "nosuchuser": "Не екзістує хоснователь з меном «$1».\nУ хосновательскых мен ся розлишують малы/великы писмена.\nСконтролюйте запис, або собі [[Special:CreateAccount|зареґіструйте нове конто]].",
        "nosuchusershort": "Хоснователь з меном $1 не екзістує.\nПеревірте правилность написаня мена.",
        "nouserspecified": "Мусите задати мено хоснователя.",
        "login-userblocked": "Тот хоснователь є заблокованый. Приголошіня не є дозволене.",
        "suspicious-userlogout": "Ваша пожадавка на одголошіня была одвергнута, бо вызерає то так, же была послана розбитым переглядачом або кешуючім проксі-сервером.",
        "createacct-another-realname-tip": "Правдиве імя є волительне.\nКідь вы зволите го додати, тото буде пак хосноване на доданя участникового попису про ёго роботу.",
        "pt-login": "Приголошіня",
+       "pt-login-button": "Приголошіня",
        "pt-createaccount": "Створити конто",
        "pt-userlogout": "Одголосити ся",
        "php-mail-error-unknown": "Незнама хыба у PHP mail() функції",
        "newpassword": "Нове гесло:",
        "retypenew": "Напиште знову нове гесло:",
        "resetpass_submit": "Наставити гесло і приголосити ся",
-       "changepassword-success": "Ваше гесло было успішно змінено!",
+       "changepassword-success": "Ваше гесло было змінено!",
        "changepassword-throttled": "Зробили сьте дуже много спроб о приголошіня.\nПросиме Вас, почекайте $1 перед далшов спробов.",
        "resetpass_forbidden": "Гесла не є можне змінити",
+       "resetpass_forbidden-reason": "Гесла не є можне змінити: $1",
        "resetpass-no-info": "Ку тій сторінцї мають прямый приступ лем приголошены хоснователї.",
        "resetpass-submit-loggedin": "Змінити гесло",
        "resetpass-submit-cancel": "Сторно",
        "blocked-notice-logextract": "Тот хоснователь є теперь блокованый.\nПослїднїй запис в лоґах блоковань є такый:",
        "clearyourcache": "'''Позначка: По уложіню мусите вымазати кеш вашого перезерача, інакше зміны не будете видїти.'''\n'''Mozilla / Firefox / Safari:''' При кликнутю на ''Актуалізовати'' тримайте ''Shift'', або стиснийте ''Ctrl-F5'' або ''Ctrl-R'' (на Macintosh ''Command-R'');\n'''Opera:''' Вымажте обсяг кеш в меню ''Інштрументы → Наставлїня'';\n'''Internet Explorer:''' При кликнутю на ''Актуалізовати'' тримайте ''Ctrl'', або стиснийте ''Ctrl-F5''.",
        "usercssyoucanpreview": "'''Тіп:''' Хоснуйте ґомбічку „{{int:showpreview}}“ про тестованя вашого нового CSS перед уложінём.",
+       "userjsonyoucanpreview": "<strong>Тіп:</strong> Хоснуйте ґомбічку „{{int:showpreview}}“ про тестованя вашого нового коду JSON перед уложінём.",
        "userjsyoucanpreview": "'''Тіп:''' Хоснуйте ґомбічку „{{int:showpreview}}“ про тестованя вашого нового коду JavaScript перед уложінём.",
        "usercsspreview": "'''Памятайте, же собі перезерате лем нагляд вашого хосновательского CSS.'''\n'''Іщі не было уложено!'''",
        "userjspreview": "'''Памятайте, же тестуєте а перезерате лем нагляд вашого хосновательского JavaScript-у, іщі не быв уложеный!'''",
        "content-not-allowed-here": "Обсяг «$1» недозволеный на сторінцї [[:$2]]",
        "editwarning-warning": "Зохаблїнём той стрінкы ся можуть вшыткы учінены зміны стратити.\nКідь сьте приголошеный, можете тото варованя выпнути на картї „Едітованя“ в хосновательскім інтерфейсї.",
        "editpage-notsupportedcontentformat-title": "Непідпорованый формат обсягу",
+       "slot-name-main": "Головна",
        "content-model-wikitext": "вікітекст",
        "content-model-text": "чістый текст",
        "content-model-javascript": "JavaScript",
        "currentrev": "Актуална ревізія",
        "currentrev-asof": "Точна ревізія на $1",
        "revisionasof": "Ревізія $1",
-       "revision-info": "Верзія од $1; $2",
-       "previousrevision": "Старша верзія",
+       "revision-info": "Верзія од $1; {{GENDER:$6|$2}}$7",
+       "previousrevision": "← Старша верзія",
        "nextrevision": "Новша ревізія →",
        "currentrevisionlink": "Актуална ревізія",
        "cur": "актуална",
        "history-feed-description": "Історія едітовань той сторінкы на вікі",
        "history-feed-item-nocomment": "$1 в $2",
        "history-feed-empty": "Такой сторінкы нїт.\nМогли єй вымазати ці переменовати.\nСпробуйте [[Special:Search|найти на вікі]] подобны сторінкы.",
+       "history-edit-tags": "Змінити значкы выбраных ревізій",
        "rev-deleted-comment": "(згорнутя едітованя вымазане)",
        "rev-deleted-user": "(імя автора стерто)",
        "rev-deleted-event": "(лоґ одстраненый)",
        "rev-deleted-user-contribs": "[мено хоснователя або IP адреса одстранене – едітованя є в приспевках сховане]",
        "rev-deleted-text-permission": "Тота ревізія была  '''вылучена''.\nДетайлы можуть быти зазначены в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} книзї вылученых сторінок].",
+       "rev-suppressed-text-permission": "Тота верзія была <strong>затаєна</strong>.\nДетайлы можуть быти зазначены в [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} записї затаїня].",
        "rev-deleted-text-unhide": "Тота ревізія была '''вылучена'''.\nДетайлы можуть быти зазначены в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} книзї вылученых сторінок].\nМожете  сі все [$1 тоту ревізію посмотрити], кідь хочете.",
        "rev-suppressed-text-unhide": "Тота ревізія была '''затаєна'''.\nДетайлы можуть быти уведены в  [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} записї утаїня].\nМожете сі [$1 тоту ревізію посмотрити], кідь  хочете.",
        "rev-deleted-text-view": "Тота ревізія была  '''вылучена'''.\nМожете сі єй посмотрити; детайлы можуть быти зазначены в [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} книзї вылученых сторінок].",
        "revdelete-no-file": "Зазначеный файл не єствує.",
        "revdelete-show-file-confirm": "На певно собі хочете посмотрити вылучену ревізію файлу „<nowiki>$1</nowiki>“ з $2, $3?",
        "revdelete-show-file-submit": "Гей",
+       "revdelete-selected-text": "{{PLURAL:$1|Выбрана ревізія|Выбраны ревізії}} [[:$2]]:",
+       "revdelete-selected-file": "{{PLURAL:$1|Выбрана верзія файлу|Выбраны верзії файлу}} [[:$2]]:",
        "logdelete-selected": "{{PLURAL:$1|Выбрана протоколована подїя|Выбраны протоколованы подїї}}:",
        "revdelete-confirm": "Просиме Вас, потвердьте, же то хочете справды зробити, же собі усвідомлюєте резултат і же є то в згодї з  [[{{MediaWiki:Policy-url}}|правилами]].",
        "revdelete-suppress-text": "Затаёваня бы ся мало хосновати  ''лем''' в такых припадах:\n* Потенціално огваряючі інформації\n* Непотрібны особны дата\n*: <em>адресы і телефонны чісла, родны чісла ітд.</em>",
        "revdelete-legend": "Наставити обмеджіня ревізії",
-       "revdelete-hide-text": "СÑ\85оваÑ\82и Ñ\82екст ревізії",
+       "revdelete-hide-text": "Текст ревізії",
        "revdelete-hide-image": "Сховати обсяг файлу",
        "revdelete-hide-name": "Сховати дїю а ціль",
        "revdelete-hide-comment": "Сховати коментарь",
        "revdelete-unsuppress": "Одстранити обмеджіня на вернутій верзії",
        "revdelete-log": "Причіна:",
        "revdelete-submit": "Апліковати на {{PLURAL:$1|зазначену ревізію|зазначены ревізії}}",
-       "revdelete-success": "'''Видимость ревізії успішно змінена.'''",
-       "revdelete-failure": "'''Не вдало ся змінити видимость ревізії:'''\n$1",
-       "logdelete-success": "'''Видимость події успішно наставена.'''",
+       "revdelete-success": "Видимость ревізії змінена.",
+       "revdelete-failure": "Не вдало ся змінити видимость ревізії:\n$1",
+       "logdelete-success": "Видимость події наставена.",
        "logdelete-failure": "'''Не вдало ся наставити видимость протоколу.'''\n$1",
        "revdel-restore": "Змінити видимость",
        "pagehist": "Історія сторінкы",
        "mergehistory-go": "Вказати злучітельны едітації",
        "mergehistory-submit": "Споїти ревізії",
        "mergehistory-empty": "Не дають ся споїти жадны ревізії.",
-       "mergehistory-done": "$3 {{PLURAL:$3|ревізія|ревізії|ревізійí}} сторінкы $1 {{PLURAL:$3|была успішно злучена|были успішно злучены|было успішно злуґено}} до сторінкы [[:$2]].",
+       "mergehistory-done": "$3 {{PLURAL:$3|ревізія|ревізії|ревізій}} сторінкы $1 {{PLURAL:$3|была успішно злучена|были успішно злучены|было успішно злучено}} до сторінкы [[:$2]].",
        "mergehistory-fail": "Злучіня історій ся не дасть зробити. Перевірте заданы сторінкы і їх історії",
        "mergehistory-no-source": "Жрідлова сторінка $1 не існує.",
        "mergehistory-no-destination": "Цілёва сторінка «$1» не екзістує.",
        "prefs-personal": "Інформації о хоснователёви",
        "prefs-rc": "Послїднї зміны",
        "prefs-watchlist": "Слїдованы сторінкы",
+       "prefs-editwatchlist": "Едітованя списку слїдованых сторінок",
        "prefs-watchlist-days": "Чісло днїв зображеных в слїдованых сторінках:",
        "prefs-watchlist-days-max": "Максімално $1 {{PLURAL:$1|день|днї|днїв}}",
        "prefs-watchlist-edits": "Чісло едітовань зображеных во вылїпшенім списку слїдованых сторінок:",
        "recentchangesdays": "За кілько днїв вказовати новы едітованя",
        "recentchangesdays-max": "(максімум $1 {{PLURAL:$1|день|днї|днїв}})",
        "recentchangescount": "Чісло імпліцітно зображованых едітовань:",
-       "prefs-help-recentchangescount": "ТÑ\8bкаÑ\82Ñ\8c Ñ\81Ñ\8f Ð¿Ð¾Ñ\81лÑ\97днÑ\97Ñ\85 Ð·Ð¼Ñ\96н, Ñ\96Ñ\81Ñ\82оÑ\80Ñ\96Ñ\97 Ñ\81Ñ\82оÑ\80Ñ\96нок Ñ\96 Ð¿Ñ\80оÑ\82околоваÑ\87Ñ\96Ñ\85 Ð·Ð°Ð¿Ð¸Ñ\81Ñ\96в.",
+       "prefs-help-recentchangescount": "Ð\9cакÑ\81Ñ\96малне Ñ\87Ñ\96Ñ\81ло: 1000",
        "prefs-help-watchlist-token2": "Гевсе є тайный кліч до вебового порталу вашых слїдованых сторінок. Хоцьхто, хто тот кліч буде мав, буде міг ваш список слїдованых сторінок чітати, та же го никому не давайте.\n[[Special:ResetTokens|Кликнутём гев го можете реініціалізовати.]]",
        "savedprefs": "Ваше наставлїня было уложене.",
        "timezonelegend": "Часова зона:",
        "default": "імпліцітне",
        "prefs-files": "Файлы",
        "prefs-custom-css": "Властный CSS",
+       "prefs-custom-json": "Властный JSON",
        "prefs-custom-js": "Властный JS",
-       "prefs-common-config": "Сдїляне CSS/JS про вшыткы штілы:",
+       "prefs-common-config": "Сдїляне CSS/JSON/JavaScript про вшыткы штілы:",
        "prefs-reset-intro": "Помочов той сторінкы можете вшыткы наставлїня вернути на імпліцітны годноты.\nТоту операцію не годен вернути назад.",
        "prefs-emailconfirm-label": "Потверджіня електронічной пошты:",
        "youremail": "Адреса електронічной пошты:",
        "username": "{{GENDER:$1|Імя хоснователя}}:",
        "prefs-memberingroups": "{{GENDER:$2|Член}} {{PLURAL:$1|ґрупы|ґруп}}:",
+       "group-membership-link-with-expiry": "$1 (до $2)",
        "prefs-registration": "Час реґістрації:",
        "yourrealname": "Правдиве імя:",
        "yourlanguage": "Язык:",
        "prefs-advancedwatchlist": "Розшырены можности",
        "prefs-displayrc": "Наставлїня  взгляду",
        "prefs-displaywatchlist": "Наставлїня  взгляду",
+       "prefs-pageswatchlist": "Слїдованы сторінкы",
        "prefs-tokenwatchlist": "Кліч",
        "prefs-diffs": "Порівнаня верзії",
        "prefs-help-prefershttps": "Тот параметер набуде чінности по вашім далшім входї до сістемы.",
        "userrights-nodatabase": "Датабаза $1 не екзістує або не є місцёва.",
        "userrights-changeable-col": "Ґрупы, котры можете змінити",
        "userrights-unchangeable-col": "Ґрупы, котры не можете змінити",
+       "userrights-expiry-othertime": "Іншый час:",
        "userrights-conflict": "Конфлікт змін прав хоснователїв! Просиме, перевірьте зміны і потвердьте їх.",
        "group": "Ґрупа:",
        "group-user": "Хоснователї",
        "group-autoconfirmed": "Автопотверджены хоснователї",
        "group-bot": "Боты",
        "group-sysop": "Адміністраторы",
+       "group-interface-admin": "Адміністраторы інтерфейсу",
        "group-bureaucrat": "Бірократы",
        "group-suppress": "Ревізоры",
        "group-all": "(вшыткы)",
        "group-autoconfirmed-member": "{{GENDER:$1|автопотвердженый хоснователь|автопотверджена хоснователька|автопотвердженый хоснователь}}",
        "group-bot-member": "{{GENDER:$1|бот|ботка|бот}}",
        "group-sysop-member": "{{GENDER:$1|адміністратор|адміністраторка|адміністратор}}",
+       "group-interface-admin-member": "{{GENDER:$1|адміністратор інтерфейсу|адміністраторка інтерфейсу}}",
        "group-bureaucrat-member": "{{GENDER:$1|бюрократ|бюрократка|бюрократ}}",
        "group-suppress-member": "{{GENDER:$1|ревізор|ревізорка|ревізор}}",
        "grouppage-user": "{{ns:project}}:Хоснователї",
        "grouppage-autoconfirmed": "{{ns:project}}:Автопотверджены хоснователї",
        "grouppage-bot": "{{ns:project}}:Боты",
        "grouppage-sysop": "{{ns:project}}:Адміністраторы",
+       "grouppage-interface-admin": "{{ns:project}}:Адміністраторы інтерфейсу",
        "grouppage-bureaucrat": "{{ns:project}}:Бірократы",
        "grouppage-suppress": "{{ns:project}}:Ревізоры",
        "right-read": "Чітаня сторінок",
        "right-createpage": "Вытворїня сторінок (не діскузных)",
        "right-createtalk": "Вытворїня сторінок діскузії",
        "right-createaccount": "Вытворїня новых конт хоснователїв",
+       "right-autocreateaccount": "Автоматічне приголошіня з екстерным хосновательскым контом",
        "right-minoredit": "Означованя  едітовань як малых",
        "right-move": "Переменованя сторінок",
        "right-move-subpages": "Переменованя сторінок і їх підсторінок",
        "right-siteadmin": "Замыкана і одомыканя датабазы",
        "right-override-export-depth": "Експорт сторінок включаючі звязаны сторінкы з глубков до 5",
        "right-sendemail": "Посыланя пошты іншым хоснователям",
+       "right-managechangetags": "Вытворїня і (де)актівація [[Special:Tags|значок]]",
        "right-applychangetags": "Придаваня [[Special:Tags|значок]] до властных змін",
+       "grant-blockusers": "Блоковати і одблоковати хоснователїв",
+       "grant-createaccount": "Закладаня конт",
+       "grant-editmywatchlist": "Едітовати ваш список слїдованых сторінок",
+       "grant-editprotected": "Едітованя замкнутых сторінок",
+       "grant-uploadfile": "Наладовати новы файлы",
+       "grant-viewmywatchlist": "Перезерати ваш список слїдованых сторінок",
        "newuserlogpage": "Лоґ вытварянь хоснователїв",
        "newuserlogpagetext": "Тото є список ново реґістрованых хоснователїв.",
        "rightslog": "Лоґ хосновательскых прав",
        "rightslogtext": "Тото є протокол зміны прав хоснователїв",
        "action-read": "прочітати тоту сторінку",
        "action-edit": "едітованя той сторінкы",
-       "action-createpage": "створїня сторінок",
-       "action-createtalk": "Вытворїня сторінок діскузії",
+       "action-createpage": "створити тоту сторінку",
+       "action-createtalk": "створити тоту діскузну сторінку",
        "action-createaccount": "Вытворїня того конта хоснователя",
        "action-minoredit": "означіти тото едітованя як мале",
        "action-move": "Переменовати тоту сторінку",
        "action-userrights-interwiki": "зміна прав хоснователїв на іншых вікі",
        "action-siteadmin": "замыкати або одомыкати датабазу",
        "action-sendemail": "посылати імейлы",
+       "action-editmyoptions": "зміна вашых хосновательскых наставлїнь",
        "action-editmywatchlist": "едітовати ваш список слїдованых сторінок",
        "action-viewmywatchlist": "перезерати ваш список слїдованых сторінок",
        "action-viewmyprivateinfo": "перезерати вашы пріватны даны",
        "action-editmyprivateinfo": "едітовати вашы пріватны інформації",
        "nchanges": "$1 {{PLURAL:$1|зміна|зміны|змін}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|од остатнёй навщівы}}",
+       "enhancedrc-history": "історія",
        "recentchanges": "Послїднї зміны",
        "recentchanges-legend": "Можности послїднїх змін",
        "recentchanges-summary": "Слїдуйте послїднї зміны на {{grammar:genitive|{{SITENAME}}}}  на тій сторінцї.",
        "recentchanges-label-unpatrolled": "Тота зміна дотеперь не была патролёвана.",
        "recentchanges-legend-heading": "<strong>Леґенда:</strong>",
        "recentchanges-legend-newpage": "$1 — нова сторінка",
+       "recentchanges-submit": "Вказати",
+       "rcfilters-activefilters-hide": "Сховати",
+       "rcfilters-activefilters-show": "Вказати",
+       "rcfilters-savedqueries-rename": "Переменовати",
+       "rcfilters-savedqueries-remove": "Вымазати",
        "rcfilters-savedqueries-new-name-label": "Назва",
+       "rcfilters-savedqueries-apply-label": "Створїня філтра",
        "rcnotefrom": "Долов суть вказаны зміны од <strong>$2</strong> (до <strong>$1</strong>).",
        "rclistfrom": "Вказати едітованя почінаючі з $3 $2.",
        "rcshowhideminor": "$1 маленькы едітованя",
        "ipbreason-dropdown": "*Часты причіны блокованя\n** Вкладаня неправдивых інформацій\n** Одстранёваня обсягу сторінок\n** Вкладаня рекламных екстерных одказів\n** Вкладаня незмыслїв\n** Застрашованя або выгрожованя\n** Знеужываня веце конт\n** Невгодне мено хоснователя",
        "ipb-hardblock": "Заборонити приголошеным хоснователям едітовати з той IP-адресы",
        "ipbcreateaccount": "Не дозволити реґістрацію новых хоснователїв",
-       "ipbemailban": "Ð\97абоÑ\80ониÑ\82и Ñ\85оÑ\81новаÑ\82елÑ\91ви Ð¿Ð¾Ñ\81Ñ\8bлаÑ\82и ÐµÐ». Ð¿Ð¾Ñ\88Ñ\82Ñ\83",
+       "ipbemailban": "Ð\9fоÑ\81Ñ\8bланÑ\8f ÐµÐ». Ð¿Ð¾Ñ\88Ñ\82Ñ\8b",
        "ipbenableautoblock": "Автоматічно блоковати IP адресы хоснованы тым хоснователём",
        "ipbsubmit": "Заблоковати",
        "ipbother": "Іншый час:",
        "ipboptions": "2 годины:2 hours,1 день:1 day,3 днї:3 days,1 тыждень:1 week,2 тыжднї:2 weeks,1 місяць:1 month,3 місяцї:3 months,6 місяцїв:6 months,1 рік:1 year,неограніченї:infinite",
        "ipbhidename": "Сховати мено хоснователя в едітованях і списках",
        "ipbwatchuser": "Слїдовати хосновательску і діскузну сторінку того хоснователя",
-       "ipb-disableusertalk": "Ð\97абоÑ\80ониÑ\82и Ñ\82омÑ\83 Ñ\85оÑ\81новаÑ\82елÑ\91ви ÐµÐ´Ñ\96Ñ\82оваÑ\82и Ð²Ð»Ð°Ñ\81Ñ\82нÑ\83 Ñ\85оÑ\81новаÑ\82елÑ\8cÑ\81кÑ\83 Ð´Ñ\96Ñ\81кÑ\83зÑ\96Ñ\8e Ð¿Ð¾ Ñ\87аÑ\81 Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ñ\8f",
+       "ipb-disableusertalk": "Ð\95дÑ\96Ñ\82ованÑ\8f Ð²Ð»Ð°Ñ\81Ñ\82ной Ð´Ñ\96Ñ\81кÑ\83зÑ\96Ñ\97",
        "ipb-change-block": "Знову заблоковати хоснователя з тыма наставлїнями",
        "ipb-confirm": "Потвердити блокованя",
        "badipaddress": "Неправилна IP адреса",
index d172d72..11e4303 100644 (file)
        "mainpage": "ᱢᱩᱬᱩᱛ ᱥᱟᱦᱴᱟ",
        "mainpage-description": "ᱢᱩᱬᱩᱛ ᱥᱟᱦᱴᱟ",
        "policy-url": "Project:ᱨᱤᱛᱤᱱᱤᱛᱤ",
-       "portal": "á±\9cᱩᱥᱴᱤ á±µá±\9aá±\9eá±\9aá±\9c á±«á±©á±­á±\9fᱹᱨ",
+       "portal": "á± á±\9aᱢᱩᱱᱤᱴᱤ á±¯á±\9aᱨᱴá±\9fá±\9e",
        "portal-url": "Project:ᱠᱷᱩᱴ ᱵᱚᱞᱚᱱ ᱦᱚᱨ",
        "privacy": "ᱩᱠᱩ ᱮᱠᱛᱤᱭᱟᱨ",
        "privacypage": "Project:ᱩᱠᱩ ᱮᱠᱛᱤᱭᱟᱨ",
        "login-throttled": "ᱟᱢ ᱫᱚ ᱢᱤᱫᱜᱷᱟᱹᱲᱤ ᱞᱟᱦᱟᱨᱮ ᱟᱭᱢᱟ ᱫᱷᱟᱣ ᱵᱚᱞᱚᱜᱮᱢ ᱠᱩᱨᱩᱢᱩᱴᱩ ᱠᱮᱫᱟ᱾ \nᱟᱨᱦᱚᱸ ᱠᱩᱨᱩᱢᱩᱴᱩᱭ ᱞᱟᱦᱟᱨᱮ ᱫᱟᱭᱟᱠᱟᱛᱮ $1 ᱛᱟᱸᱜᱤᱭᱢᱮ᱾",
        "login-abort-generic": "ᱟᱢᱟᱜ ᱵᱷᱤᱛᱨᱤ ᱵᱚᱠᱟᱜ ᱫᱚ ᱵᱟᱝ ᱦᱩᱭᱞᱮᱱᱟ - ᱵᱟᱫᱽᱱᱟ",
        "loginlanguagelabel": "ᱯᱟᱹᱨᱥᱤ: $1",
-       "pt-login": "á±µá±\9aá±\9eá±\9aá±\9c á±«á±©á±­ᱟᱹᱨ",
+       "pt-login": "á±µá±\9aá±\9eá±\9aá±\9c á±«á±©á±£ᱟᱹᱨ",
        "pt-login-button": "ᱵᱚᱞᱚᱜ ᱢᱮ",
        "pt-login-continue-button": "ᱞᱮᱛᱟᱲ ᱵᱚᱞᱚ ᱠᱚᱜᱼᱢᱮ",
        "pt-createaccount": "ᱮᱠᱟᱶᱩᱴ ᱛᱮᱭᱟᱨᱢᱮ",
index 53619ca..28fedc4 100644 (file)
        "right-managechangetags": "Pravljenje i (de)aktiviranje [[Special:Tags|oznaka]]",
        "right-applychangetags": "Primijeni [[Special:Tags|oznake]] na nečije izmjene",
        "right-changetags": "Dodavanje ili uklanjanje raznih [[Special:Tags|oznaka]] na pojedinačnim verzijama i unosima zapisnika",
+       "grant-editinterface": "Uređivanje imenskog prostora \"MediaWiki\" i JSON za cijelo wiki/za korisnika",
+       "grant-editmycssjs": "Uređivanje Vašeg korisničkog CSS/JSON/JavaScripta",
+       "grant-editsiteconfig": "Uređivanje CSS/JS za cijelo wiki i za korisnika",
        "newuserlogpage": "Registar novih korisnika",
        "newuserlogpagetext": "Ovo je evidencija registracije novih korisnika.",
        "rightslog": "Evidencija korisničkih prava",
        "pager-older-n": "{{PLURAL:$1|starija 1|starije $1}}",
        "suppress": "Sakrij",
        "querypage-disabled": "Ova posebna stranica je onemogućena jer smanjuje performanse.",
-       "apihelp": "API pomoć",
+       "apihelp": "Pomoć s prilogom",
        "apihelp-no-such-module": "Modul \"$1\" nije pronađen.",
        "booksources": "Književni izvori",
        "booksources-search-legend": "Traži književne izvore",
index 9614daf..74d4ab6 100644 (file)
@@ -35,7 +35,7 @@
        "tog-forceeditsummary": "Ayyit tini iɣ ur iwiɣ imsmun n imbdln",
        "tog-watchlisthideown": "hbo ghayli bdlgh gh omdfor inu",
        "tog-watchlisthidebots": "hba ghayli bdln robotat gh omdfor inu",
-       "tog-watchlisthideminor": "Ḥbu ibdln mzinin ɣ umdfur inu",
+       "tog-watchlisthideminor": "ⵙⵙⵏⵜⵍ ⵉⵙⵏⴼⵉⵍⵏ ⵎⵥⵥⵉⵏⵉⵏ ⵣⵖ ⵜⴰⵍⵉⵙⵜⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
        "tog-watchlisthideliu": "Ḥbu ibdln n wili skrn midn llin iqqiydn ɣu umdfr inu.",
        "tog-watchlisthideanons": "Ḥbu ibdl n midn lli urittuyssanin ɣ umdfr inu",
        "tog-watchlisthidepatrolled": "Ḥbu ibdln lli nssugga  ɣu umuɣ n umdfr",
        "editfont-serif": "ⵜⵉⵙⵉⵙⴽⵉⵍⵜ ⵙⵉⵔⵉⴼ",
        "sunday": "ⵍⵃⴷⴷ",
        "monday": "ⵍⵜⵏⵉⵏ",
-       "tuesday": "â´°âµ\99âµ\89âµ\8fâ´°âµ\99",
-       "wednesday": "âµ\8dâ´°âµ\94â´±âµ\84",
+       "tuesday": "âµ\9fâµ\9fâµ\8dâ´°âµ\9câ´°",
+       "wednesday": "âµ\8dâµ\84âµ\94ⴱⴰ",
        "thursday": "ⵍⵅⵎⵉⵙ",
        "friday": "ⵍⵊⴰⵎⵄ",
        "saturday": "ⵙⵙⴱⵜ",
        "sun": "ⵍⵃⴷⴷ",
        "mon": "ⵍⵜⵏⵉⵏ",
        "tue": "ⵟⵟⵍⴰⵜⴰ",
-       "wed": "âµ\8dâ´°âµ\94â´±âµ\84",
+       "wed": "âµ\8dâµ\84âµ\94ⴱⴰ",
        "thu": "ⵍⵅⵎⵉⵙ",
        "fri": "ⵍⵊⴰⵎⵄ",
        "sat": "ⵙⵙⴱⵜ",
        "january": "ⵉⵏⵏⴰⵢⵔ",
-       "february": "ⴼⴱâµ\95â´°âµ¢âµ\8d",
-       "march": "âµ\8eâ´°âµ\95ⵙ",
+       "february": "â´±âµ\94â´°âµ¢âµ\94",
+       "march": "âµ\8eâ´°âµ\94ⵙ",
        "april": "ⴰⴱⵔⵉⵍ",
        "may_long": "ⵎⴰⵢⵢⵓ",
        "june": "ⵢⵓⵏⵢⵓ",
        "july": "ⵢⵓⵍⵢⵓⵣ",
        "august": "ⵖⵓⵛⵜ",
        "september": "ⵛⵓⵜⴰⵎⴱⵉⵔ",
-       "october": "â´½âµ\9câµ\93â´±âµ\95",
+       "october": "â´½âµ\9câµ\93â´±âµ\94",
        "november": "ⵏⵓⵡⴰⵎⴱⵉⵔ",
        "december": "ⴷⵓⵊⴰⵎⴱⵉⵔ",
        "january-gen": "ⵉⵏⵏⴰⵢⵔ",
-       "february-gen": "ⴼⴱâµ\95â´°âµ¢âµ\8d",
-       "march-gen": "âµ\8eâ´°âµ\95ⵙ",
+       "february-gen": "â´±âµ\94â´°âµ¢âµ\94",
+       "march-gen": "âµ\8eâ´°âµ\94ⵙ",
        "april-gen": "ⴰⴱⵔⵉⵍ",
        "may-gen": "ⵎⴰⵢⵢⵓ",
        "june-gen": "ⵢⵓⵏⵢⵓ",
        "july-gen": "ⵢⵓⵍⵢⵓⵣ",
        "august-gen": "ⵖⵓⵛⵜ",
        "september-gen": "ⵛⵓⵜⴰⵎⴱⵉⵔ",
-       "october-gen": "â´½âµ\9câµ\93â´±âµ\95",
+       "october-gen": "â´½âµ\9câµ\93â´±âµ\94",
        "november-gen": "ⵏⵓⵡⴰⵎⴱⵉⵔ",
        "december-gen": "ⴷⵓⵊⴰⵎⴱⵉⵔ",
        "jan": "ⵉⵏⵏ",
-       "feb": "ⴼⴱâµ\95",
-       "mar": "âµ\8eâ´°âµ\95",
+       "feb": "â´±âµ\94â´°",
+       "mar": "âµ\8eâ´°âµ\94",
        "apr": "ⴰⴱⵔ",
        "may": "ⵎⴰⵢ",
        "jun": "ⵢⵓⵏ",
        "category-article-count-limited": "{{PLURAL:$1|Tasna d yuckan tlla|Tisniwin $1 llid yuckan llant}} ɣ taggayt ad",
        "category-file-count": "ⵜⴰⴳⴳⴰⵢⵜ ⴰⴷ {{PLURAL:$2|ⵓⵔ ⵜⵓⵎⵢ ⴰⴱⵍⴰ ⴰⴼⴰⵢⵍⵓ ⴰⴷ ⵏⵏⴰ ⵉⴹⴼⴰⵔⵏ.|ⵜⵓⵎⵢ $2 ⵉⴼⴰⵢⵍⵓⵜⵏ, ⴳⵉⵙⵏ {{PLURAL:$1|ⴰⴼⴰⵢⵍⵓ ⴰⴷ ⵏⵏⴰ ⵉⴹⴼⴰⵔⵏ|$1 ⵉⴼⴰⵢⵍⵓⵜⵏ ⴰⴷ ⵏⵏⴰ ⴹⴼⴰⵔⵏⵉⵏ}} ⴷⴷⴰⵡ ⴰⵙ.}}",
        "category-file-count-limited": "{{PLURAL:$1|Asdaw ad yuckan illa|isdawn ad $1 llid yuckan llan}} ɣ taggayt ad",
-       "listingcontinuesabbrev": "Attfr",
+       "listingcontinuesabbrev": "ⵎⴰⴷ ⵉⴹⴼⴰⵔⵏ",
        "index-category": "ⵜⴰⵙⵡⵏⵉⵡⵉⵏ ⵜⵜⵡⴰⵏⴷⵉⴽⵙⴰⵏⵉⵏ",
        "noindex-category": "Tisniwin bla amatar",
        "broken-file-category": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⵖ ⵍⵍⴰⵏ ⵉⵍⵉⵏⴽⵏ ⵔⵥⴰⵏⵉⵏ",
-       "about": "â´°ⴼ",
+       "about": "âµ\96ⴼ",
        "article": "ⵜⴰⵙⵏⴰ ⵏ ⵜⵓⵎⴰⵢⵜ",
-       "newwindow": "(âµ\89âµ\9câµ\9câµ\8fâµ\93âµ\94âµ¥âµ\93âµ\8e â´·â´³ ⵓⵙⴽⵙⵍ ⴰⵎⴰⵢⵏⵓ)",
+       "newwindow": "(â´°âµ\94 âµ\89âµ\9câµ\9câµ\8fâµ\93âµ\94âµ¥âµ\93âµ\8e âµ\96 ⵓⵙⴽⵙⵍ ⴰⵎⴰⵢⵏⵓ)",
        "cancel": "ⵙⵎⵎⵜ",
        "moredotdotdot": "ⵓⴳⴳⴰⵔ...",
        "mypage": "ⵜⴰⵙⵏⴰ",
        "and": "&#32;ⴷ",
        "faq": "ⵉⵇⵙⵇⵙⵉⵜⵏ ⵜⵜⵢⴰⵍⴰⵙⵏⵉⵏ",
        "actions": "ⵜⵉⴳⴰⵡⵉⵏ",
-       "namespaces": "Ismawn n tɣula",
+       "namespaces": "ⵉⴳⵔⴰⵏ",
        "variants": "ⵜⵉⵎⵣⴰⵔⴰⵢⵉⵏ",
        "errorpagetitle": "ⵜⴰⵣⴳⵍⵜ",
        "returnto": "ⵉⵡⵔⵔⵉ ⵏⵏ ⵙ $1.",
        "categorypage": "ⵥⵔ ⵜⴰⵙⵏⴰ ⵏ ⵜⴰⴳⴳⴰⵢⵜ",
        "viewtalkpage": "ⵥⵔ ⴰⵎⵙⴰⵡⴰⵍ",
        "otherlanguages": "ⵙ ⵜⵓⵜⵍⴰⵢⵉⵏ ⵢⴰⴹⵏ",
-       "redirectedfrom": "(Tmmuttid z $1)",
-       "redirectpagesub": "Tasna n-usmmattay",
-       "redirectto": "â´°âµ\99âµ\8eâ´°âµ\9câµ\9câµ¢ âµ\99:",
+       "redirectedfrom": "(ⵉⴽⴽⴰ ⴷ $1)",
+       "redirectpagesub": "ⵜⴰⵙⵏⴰ ⵏ ⵓⵙⵎⴰⵜⵜⵉ",
+       "redirectto": "â´°âµ\99âµ\8eâ´°âµ\9câµ\9câµ\89 âµ\99 :",
        "lastmodifiedat": "ⴰⵙⵏⴼⵍ ⵉⴳⴳⵯⵔⴰⵏ ⵖ ⵜⴰⵙⵏⴰ ⴰⴷ ⵉⵜⵜⵢⴰⵡⵙⴽⴰⵔ ⴰⵙⵙ ⵏ $1 ⵖ $2.",
        "viewcount": "Tmmurzm tasna yad {{PLURAL:$1|yat twalt|$1 mnnawt twal}}.",
        "protectedpage": "Tasnayat iqn ugdal nes.",
-       "jumpto": "ⴷⴷⵓ ⵙ:",
+       "jumpto": "ⴷⴷⵓ ⵙ :",
        "jumptonavigation": "ⴰⵙⵜⴰⵔⴰ",
        "jumptosearch": "ⵙⵉⴳⴳⵍ",
-       "view-pool-error": "âµ\99âµ\99âµ\93âµ\94â´¼ â´°âµ\99, âµ\89âµ\99âµ\89âµ\94â´±âµ\93âµ\94âµ\8f âµ\95âµ\8eâµ\89âµ\8f âµ\96âµ\89âµ\8dâ´°â´·.\nⴱⵣⵣⴰⴼ âµ\8f âµ\89âµ\99âµ\8eâµ\94â´°âµ\99âµ\8f â´°âµ\94 âµ\9câµ\9câ´°âµ\94âµ\8eâµ\8f â´°â´· â´°âµ\8fâµ\8fâ´°âµ¢âµ\8f âµ\9câ´°âµ\99âµ\8fâ´° â´°â´·.\nâµ\87âµ\87âµ\8d âµ¢â´°âµ\8f âµ\89âµ\8eâµ\89ⴽⴽ â´¼â´°â´· â´°â´· â´·â´°âµ\96 âµ\9câ´°âµ\94âµ\8eâµ\9c â´°â´· âµ\9câ´½âµ\9bâµ\8eâµ\9c âµ\99 âµ\9câ´°âµ\99âµ\8fâ´° â´°â´·.\n\n$1",
+       "view-pool-error": "âµ\99âµ\99âµ\93âµ\94â´¼ â´°âµ\99, âµ\89âµ\99âµ\89âµ\94â´±âµ\93âµ\94âµ\8f âµ\95âµ\8eâµ\89âµ\8f âµ\96âµ\89âµ\8dâ´°â´·.\nâ´½âµ\89ⴳⴰâµ\8f âµ\8f âµ\89âµ\99âµ\8eâµ\94â´°âµ\99âµ\8f âµ\94â´°âµ\8f âµ\96âµ\89âµ\8dâ´°â´· â´°â´· â´°âµ\8fâµ\8fâ´°âµ¢âµ\8f âµ\9câ´°âµ\99âµ\8fâ´° â´°â´·.\nâµ\87âµ\87âµ\8d âµ¢â´°âµ\8f âµ\89âµ\8eâµ\89ⴽⴽ, âµ\9câ´°âµ\94âµ\8eâµ\9c â´°â´· âµ\8fâµ\8f â´·â´°âµ\96 âµ\99âµ\94âµ\99 âµ\9câ´½âµ\9bâµ\8eâµ\9c.\n\n$1",
        "pool-timeout": "Tzrit tizi n uql lli yak ittuykfan. Ggutn midn lli iran ad iẓr tasna yad. Urrid yan imik..",
        "pool-queuefull": "Umuɣ n twuri iẓun (iεmr)",
        "pool-errorunknown": "Anzri (error) ur ittuyssan.",
        "copyrightpage": "{{ns:project}}:ⵉⵣⵔⴼⴰⵏ ⵏ ⵓⵎⴳⴰⵢ",
        "currentevents": "ⵉⵏⵖⵎⵉⵙⵏ",
        "currentevents-url": "Project:ⵉⵏⵖⵎⵉⵙⵏ",
-       "disclaimers": "ⵉⵙⵎⵉⴳⵍⵏ",
+       "disclaimers": "Disclaimers",
        "disclaimerpage": "Project:ⴰⵙⵎⵉⴳⵍ ⴰⵎⴰⵜⴰⵢ",
-       "edithelp": "âµ\9câµ\89ⵡâµ\89âµ\99âµ\89 â´·â´³ ⵓⵙⵏⴼⵍ",
+       "edithelp": "âµ\9câµ\89ⵡâµ\89âµ\99âµ\89 âµ\96 ⵓⵙⵏⴼⵍ",
        "helppage-top-gethelp": "ⵜⵉⵡⵉⵙⵉ",
        "mainpage": "ⵜⴰⵙⵏⴰ ⵏ ⵓⵙⵏⵓⴱⴳ",
        "mainpage-description": "ⵜⴰⵙⵏⴰ ⵏ ⵓⵙⵏⵓⴱⴳ",
        "collapsible-expand": "Sfruri",
        "confirmable-yes": "ⵢⴰⵀ",
        "confirmable-no": "ⵓⵀⵓ",
-       "thisisdeleted": "ⵉⵙ ⵜⵔⵉⵜ ⴰⴷ ⵜⴰⵏⵏⴰⵢⵜ ⵏⵖ ⴰⴷ ⵜⵙⵙⴰⴹⵓⵜ $1?",
-       "viewdeleted": "ⴰⴷ ⵜⵥⵔⵜ $1?",
+       "thisisdeleted": "ⵉⵙ ⵜⵔⵉⵜ ⴰⴷ ⵜⴰⵏⵏⴰⵢⵜ ⵏⵖ ⴰⴷ ⵜⵙⵙⴰⴹⵓⵜ $1 ?",
+       "viewdeleted": "ⴰⴷ ⵜⵥⵔⵜ $1 ?",
        "restorelink": "{{PLURAL:$1|ⵢⴰⵏ ⵓⵙⵏⴼⵍ ⵉⵜⵜⵡⴰⴽⴽⵙⵏ|$1 ⵉⵙⵏⴼⵉⵍⵏ ⵜⵜⵡⴰⴽⴽⵙⵏⵉⵏ}}",
        "feedlinks": "ⵉⴼⵉⵍⵉ:",
        "feed-invalid": "Anaw n usurdm ur gis iffuy umya",
        "site-rss-feed": "ⵉⴼⵉⵍⵉ RSS ⵏ $1",
        "site-atom-feed": "ⵉⴼⵉⵍⵉ ⴰⵡⵟⵓⵎ ⵏ $1",
        "page-rss-feed": "ⵉⴼⵉⵍⵉ RSS ⵏ \"$1\"",
-       "page-atom-feed": "ⵉⴼⵉⵍⵉ ⴰⵟⵓⵎ ⵏ \"$1\"",
-       "red-link-title": "$1 (âµ\9câ´°âµ\99âµ\8fâ´° âµ\93âµ\94 âµ\89âµ\8dâµ\8dâµ\89âµ\8f)",
+       "page-atom-feed": "âµ\89â´¼âµ\89âµ\8dâµ\89 â´°âµ¡âµ\9fâµ\93âµ\8e âµ\8f \"$1\"",
+       "red-link-title": "$1 (âµ\93âµ\94 âµ\9câµ\8dâµ\8dâµ\89 âµ\9câ´°âµ\99âµ\8fâ´° â´°â´·)",
        "nstab-main": "ⵜⴰⵙⵏⴰ",
        "nstab-user": "ⵜⴰⵙⵏⴰ ⵏ {{GENDER:{{ROOTPAGENAME}}|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}}",
-       "nstab-media": "Tasnat Ntuzumt",
+       "nstab-media": "ⵎⵉⴷⵢⴰ",
        "nstab-special": "ⵜⴰⵙⵏⴰ ⵉⵥⵍⵉⵏ",
        "nstab-project": "Project page",
        "nstab-image": "ⴰⴼⴰⵢⵍⵓ",
        "nstab-mediawiki": "ⵜⵓⵣⵉⵏⵜ",
-       "nstab-template": "Talɣa",
+       "nstab-template": "ⴰⵍⴱⵓⴹ",
        "nstab-help": "ⵜⴰⵙⵏⴰ ⵏ ⵜⵡⵉⵙⵉ",
        "nstab-category": "ⵜⴰⴳⴳⴰⵢⵜ",
        "mainpage-nstab": "ⵜⴰⵙⵏⴰ ⵏ ⵓⵙⵏⵓⴱⴳ",
        "nosuchaction": "ⵓⵔ ⵜⵍⵍⵉ ⵜⵉⴳⴰⵡⵜ ⴰⴷ",
        "nosuchactiontext": "Mytuskarn ɣu tansa yad ur tti tgi.\nⵉⵔⵡⴰⵙ ⵉⵙ ⵓⵔ ⵜⵓⵔⵉⵜ ⵎⵣⵢⴰⵏ ⴰURL, ⵏⵖ ⵉⵙ ⵜⴹⴼⵔⵜ ⴽⵔⴰ ⵏ ⵓⵍⵉⵏⴽ ⵉⵣⴳⵍⵏ.\nTzdar attili tamukrist ɣ {{SITENAME}}.",
        "nosuchspecialpage": "ⵓⵔ ⵜⵍⵍⵉ ⵜⴰⵙⵏⴰ ⴰⴷ ⵉⵥⵍⵉⵏ",
-       "nospecialpagetext": "<strong>âµ\9câ´»âµ\9câµ\9câµ\94âµ\9c âµ¢â´°âµ\9c âµ\9câ´°âµ\99âµ\8fâ´° âµ\89âµ¥âµ\8dâµ\89âµ\8f âµ\93âµ\94 âµ\89âµ\8dâµ\8dâµ\89âµ\8f.</strong>\n\nâµ\94â´°â´· âµ\9câ´°â´¼âµ\9c âµ¢â´°âµ\9c âµ\9câµ\8dⴳⴰâµ\8eⵜ ⵏ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵥⵍⵉⵏⵉⵏ ⵖⵜⴰⵏⵉⵏ ⵖ [[Special:SpecialPages|{{int:specialpages}}]].",
+       "nospecialpagetext": "<strong>âµ\9cⴹⴰâµ\8dâ´±âµ\9c âµ\8fâµ\8f âµ¢â´°âµ\9c âµ\9câ´°âµ\99âµ\8fâ´° âµ\89âµ¥âµ\8dâµ\89âµ\8f âµ\93âµ\94 âµ\89âµ\8dâµ\8dâµ\89âµ\8f.</strong>\n\nâµ\94â´°â´· âµ\9câ´°â´¼âµ\9c âµ¢â´°âµ\9c âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵥⵍⵉⵏⵉⵏ ⵖⵜⴰⵏⵉⵏ ⵖ [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "ⵜⴰⵣⴳⵍⵜ",
        "databaseerror": "ⵜⴰⵣⴳⵍⵜ ⴳ ⵜⴰⵙⵉⵍⴰ ⵏ ⵉⵙⴼⴽⴰ",
        "databaseerror-error": "ⴰⵣⴳⴳⴰⵍ: $1",
        "laggedslavemode": "Ḥan tasnayad ur gis graygan ambddel amaynu.",
        "readonly": "ⵜⴰⵙⵉⵍⴰ ⵏ ⵉⵙⴼⴽⴰ ⵉⵜⵜⵡⴰⵔⴳⵍⵏ",
        "missing-article": "lqaa'ida n lbayanat ortofa nass ad gh tawriqt  liss ikhssa asti taf limism \"$1\" $2.\n\nghikad artitsbib  igh itabaa lfrq aqdim nghd tarikh artawi skra nsfha ityohyadn.\n\nighor iga lhal ghika ati ran taft kra lkhata gh lbarnamaj.\n\nini mayad ikra [[Special:ListUsers/sysop|lmodir]] tfktas ladriss ntwriqt an.",
-       "missingarticle-rev": "(lmorajaaa#: $1)",
-       "missingarticle-diff": "(ⴰⵎⵣⴰⵔⴰⵢ: $1, $2)",
-       "internalerror": "âµ\9cⴰⵣⴳâµ\8dâµ\9c âµ\9cⴰⴳⵯâµ\8fâµ\99â´°âµ\8fâµ\9c",
-       "internalerror_info": "âµ\9cⴰⵣⴳâµ\8dâµ\9c âµ\9cⴰⴳⵯâµ\8fâµ\99â´°âµ\8fâµ\9c: $1",
+       "missingarticle-rev": "(ⵓⵟⵟⵓⵏ ⵏ ⵓⵣⵣⵔⴰⵢ : $1)",
+       "missingarticle-diff": "(ⴰⵎⵣⴰⵔⴰⵢ : $1, $2)",
+       "internalerror": "ⴰⵣⴳⴳⴰâµ\8d â´°â´³âµ¯âµ\8fâµ\99â´°âµ\8f",
+       "internalerror_info": "ⴰⵣⴳⴳⴰâµ\8d â´°â´³âµ¯âµ\8fâµ\99â´°âµ\8f : $1",
        "filecopyerror": "orimkin ankopi \"$1\" s \"$2\".",
-       "filerenameerror": "ur as tufit ad tsmmut \"$1\" s \"$2\".",
+       "filerenameerror": "ⵓⵔ ⵢⴰⵍⵍⴼⵓⵙ ⴰⴷ ⵜⴰⵍⵙⵜ ⵉ ⵉⵙⵎ ⵏ \"$1\" ⵙ \"$2\".",
        "filedeleteerror": "ⵓⵔ ⵢⴰⵍⵍⴼⵓⵙ ⴰⴷ ⵉⵜⵜⵡⴰⴽⴽⵙ ⵓⴼⴰⵢⵍⵓ \"$1\".",
        "directorycreateerror": "Ur as tufit an tgt asddaw « $1 ».",
-       "filenotfound": "ⵓⵔ ⵢⴰⵍⵍⴼⵓⵙ ⴰⴷ ⵉⵜⵜⵢⴰⴼ ⵓⴼⴰⵢⵍⵓ \"$1\".",
+       "filenotfound": "ⵓⵔ ⵢⴰⵍⵍⴼⵓⵙ ⴰⴷ ⵉⵜⵜⵢⴰⴼ ⵓⴼⴰⵢⵍⵓ \"$1\".",
        "unexpected": "Azal (atig) llis ur nql: « $1 » = « $2 ».",
        "formerror": "Anzri: ur as tufit an tgt tifrkit",
        "badarticleerror": "Tigawt ad ur  as tufi ad ttuyskar ɣ tasna yad.",
        "cannotdelete": "Ur as tufa tasna yad ad ttuykkas niɣd asdaw ad « $1 ».\nAkks ad iskrt kra yaḍn",
-       "badtitle": "Azwl ur ifulkin",
+       "badtitle": "ⴰⵣⵡⵓⵍ ⵉⴼⵍⵍⵙⵏ",
        "badtitletext": "Azwl n tasna lli trit ur igadda, ixwa, niɣd iga aswl n gr tutlayt niḍ ngr tuwwurins ur izdimzyan. Ẓr urgis tgit kra nu uskkil niɣd mnnaw lli gis ur llanin",
        "viewsource": "ⵥⵔ ⴰⵙⴰⴳⵎ",
-       "virus-unknownscanner": "â´°âµ\8eâ´³âµ\8dâ´±âµ\89âµ\94âµ\93âµ\99 â´°âµ\94âµ\93âµ\99âµ\99âµ\89âµ\8f:",
-       "welcomeuser": "ⴱⵔⵔⴽ ⴰ $1!",
-       "yourname": "ⵉⵙⵎ ⵏ ⵓⵙⵎⵔⴰⵙ:",
-       "yourpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ:",
+       "virus-unknownscanner": "â´°âµ\8eâ´³âµ\8dâ´±âµ\89âµ\94âµ\93âµ\99 âµ\93âµ\94 âµ\89âµ\9câµ\9cⵢⴰⵡâµ\99âµ\99â´°âµ\8fâµ\8f :",
+       "welcomeuser": "ⴱⵔⵔⴽ ⴰ $1 !",
+       "yourname": "ⵉⵙⵎ ⵏ ⵓⵙⵎⵔⴰⵙ :",
+       "yourpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ :",
        "userlogin-yourpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ",
-       "yourpasswordagain": "ⴰⵔⴰ ⴷⴰⵖ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ:",
-       "yourdomainname": "Taɣult nek",
+       "yourpasswordagain": "ⴰⵔⴰ ⴷⴰⵖ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ :",
+       "yourdomainname": "ⵉⴳⵔ ⵏⵏⴽ :",
        "externaldberror": "Imma tlla ɣin kra lafut ɣu ukcumnk ulla urak ittuyskar at tsbddelt lkontnk nbrra.",
        "login": "ⴽⵛⵎ",
        "nav-login-createaccount": "ⴽⵛⵎ / ⵙⵏⵓⵍⴼⵓ ⴰⵎⵉⴹⴰⵏ",
        "createacct-reason": "ⵜⴰⵎⵏⵜⵉⵍⵜ",
        "createacct-benefit-body1": "{{PLURAL:$1|ⴰⵙⵏⴼⵍ|ⵉⵙⵏⴼⵉⵍⵏ}}",
        "createacct-benefit-body2": "{{PLURAL:$1|ⵜⴰⵙⵏⴰ|ⵜⴰⵙⵏⵉⵡⵉⵏ}}",
-       "createacct-benefit-body3": "{{PLURAL:$1|â´°âµ\8fâ´°âµ\8eâµ\93 âµ\89ⴳⴳⵯâµ\94â´°âµ\8f|âµ\89âµ\8fâ´°âµ\8eâµ\93âµ\9cⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ}}",
+       "createacct-benefit-body3": "{{PLURAL:$1|â´°âµ\8eâ´·âµ\94ⴰⵡ âµ\89ⴳⴳⵯâµ\94â´°âµ\8f|âµ\89âµ\8eâ´·âµ\94ⴰⵡⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ}}",
        "badretype": "ⵜⵉⴳⵓⵔⵉⵡⵉⵏ ⵏ ⵓⵣⵔⴰⵢ ⵏⵏⴰ ⵜⵙⵙⴽⵛⵎⵜ ⵓⵔ ⵎⵙⴰⵙⴰⵏⵜ.",
        "userexists": "Asaɣ nu umsqdac li tskcmt illa yad",
-       "loginerror": "Gar akccum",
+       "loginerror": "ⴰⵣⴳⴳⴰⵍ ⵖ ⵓⵣⴷⴰⵢ",
        "createaccounterror": "$1 ur as tufit at kcmt",
        "loginsuccesstitle": "ⵜⵣⴷⵉⵜ ⵏⵏ",
        "loginsuccess": "<strong>ⵀⴰ ⴽ(ⵎ) ⵉⵏⵏ ⵖⵉⵍⴰⴷ ⵜⵣⴷⵉⵜ ⵏⵏ ⵖ {{SITENAME}} ⵙ ⵢⵉⵙⵎ ⵏ \"$1\".</strong>",
        "nosuchuser": "Asqdac « $1 » ur illi.\nUssaɣ n isqdacn ḥiln hlli.\nẒṛ daɣ ist turit mzyan mayad, niɣd [[Special:CreateAccount|tmmurẓmt amiḍan amaynu]].",
        "nosuchusershort": "Ur illa umsaws lli ilan assaɣ « $1 ». Ẓṛ ist turit mzyan mayad.",
-       "nouserspecified": "Illa fllak ad tarat assaɣ nk.",
+       "nouserspecified": "ⵉⵇⵇⴰⵏ ⴽ ⵉⴷ ⴰⴷ ⴰⴷ ⵜⴰⵔⴰⵜ ⵉⵙⵎ ⵏⵏⴽ ⵏ ⵓⵙⵎⵔⴰⵙ.",
        "login-userblocked": "Asqdac ad ur as yufi ad ikcm. Tazdayt ɣ ifalan uras ttuyskar",
        "mailmypassword": "ⴰⵍⵙ ⵉ ⵜⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ",
        "mailerror": "Gar azn n tbrat : $1",
        "emailconfirmlink": "Als i tasna nk n tbratin izd nit nttat ayan.",
-       "loginlanguagelabel": "ⵜⵓⵜⵍⴰⵢⵜ: $1",
-       "pt-login": "ⵣⴷⵢ ⵏⵏ",
+       "loginlanguagelabel": "ⵜⵓⵜⵍⴰⵢⵜ : $1",
+       "pt-login": "ⵣⴷâµ\89 ⵏⵏ",
        "pt-login-button": "ⴽⵛⵎ",
        "pt-userlogout": "ⴼⴼⵖ",
        "php-mail-error-unknown": "Kra ur igadda tasɣnt btbratin() n PHP.",
        "resetpass_announce": "Tkcmt {{GENDER:||e|(e)}} s yat tangalt lli kin ilkmt s tbrat emeil . tangaltad ur tgi abla tin yat twalt. Bac ad tkmlt tqqiyyidank kcm tangalt tamaynut nk ɣid:",
        "resetpass_header": "ⵙⵏⴼⵍ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵏ ⵓⵎⵉⴹⴰⵏ",
        "oldpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵉⵇⴷⵎⵏ:",
-       "newpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵜⴰⵎⴰⵢⵏⵓⵜ:",
-       "retypenew": "ⴰⵔⴰ ⴷⴰⵖ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵜⴰⵎⴰⵢⵏⵓⵜ:",
+       "newpassword": "ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵜⴰⵎⴰⵢⵏⵓⵜ :",
+       "retypenew": "ⴰⵔⴰ ⴷⴰⵖ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵔⴰⵢ ⵜⴰⵎⴰⵢⵏⵓⵜ :",
        "resetpass_submit": "Sbadl awal n uzri tkcmt",
        "changepassword-success": "Awal n uzri nk ibudl mzyan! rad nit tilit ɣ ifalan",
        "botpasswords-label-create": "ⵙⵏⵓⵍⴼⵓ",
        "resetpass-submit-cancel": "ⵙⵎⵎⵜ",
        "resetpass-wrong-oldpass": "Awal n uzri yad niɣt walli yak ittkfan i yatwalt ur gis iffuɣ umya.\nHan irwas is yad tsbadlt awal n uzri niɣd is ḍalbt yan yaḍn n yat tklit.",
        "resetpass-temp-password": "Awal ad n uzri iga ɣir win yat tklit:",
-       "bold_sample": "Aḍṛiṣ iḍnin",
-       "bold_tip": "Aḍṛiṣ iḍnin",
-       "italic_sample": "Aḍṛiṣ iknan",
-       "italic_tip": "Aḍṛiṣ iknan",
+       "bold_sample": "ⴰⴹⵔⵉⵙ ⵉⵣⵓⵔⵏ",
+       "bold_tip": "ⴰⴹⵔⵉⵙ ⵉⵣⵓⵔⵏ",
+       "italic_sample": "ⴰⴹⵔⵉⵙ ⵉⴽⵯⵏⴰⵏ",
+       "italic_tip": "ⴰⴹⵔⵉⵙ ⵉⴽⵯⵏⴰⵏ",
        "link_sample": "ⴰⵣⵡⵍ ⵏ ⵓⵍⵉⵏⴽ",
        "link_tip": "ⴰⵍⵉⵏⴽ ⴰⴳⵯⵏⵙⴰⵏ",
-       "extlink_sample": "http://www.example.com ⴰⵣⵡⵍ ⵏ ⵓⵍⵉⵏⴽ",
-       "extlink_tip": "â´°âµ\8dâµ\89âµ\8fâ´½ â´°â´±âµ\95âµ\95â´°âµ\8f (â´°â´· âµ\93âµ\94 âµ\9câ´»âµ\9câµ\9câµ\93âµ\9c â´°ⵣⵡⵉⵔ http://)",
-       "headline_sample": "Aḍṛiṣ n ddu uzwl",
+       "extlink_sample": "http://www.example.com â´°âµ£âµ¡âµ\93âµ\8d âµ\8f âµ\93âµ\8dâµ\89âµ\8fâ´½",
+       "extlink_tip": "â´°âµ\8dâµ\89âµ\8fâ´½ â´°â´±âµ\94âµ\94â´°âµ\8fâµ\89âµ¢ (â´°â´· âµ\93âµ\94 âµ\9câ´»âµ\9câµ\9câµ\93âµ\9c âµ\93ⵣⵡⵉⵔ http://)",
+       "headline_sample": "ⴰⴹⵔⵉⵙ ⵏ ⵓⵣⵡⵓⵍ",
        "headline_tip": "Ddu-uzwl taskfalt 2",
        "nowiki_sample": "Kcm aḍṛiṣ li ur imzln ɣid",
        "nowiki_tip": "Zri Taseddast n wiki",
        "media_tip": "ⴰⵍⵉⵏⴽ ⵏ ⵓⴼⴰⵢⵍⵓ",
        "sig_tip": "ⴰⵙⴳⵎⴹ ⵏⵏⴽ/ⵎ ⵙ ⵜⵉⵣⵉ",
        "hr_tip": "izriri iɣzzifn (ⴰⴷ ⵜ ⵓⵔ ⵜⵙⵙⵓⴳⵜⵜ)",
-       "summary": "âµ\9câµ\93ⴳⵣâµ\89âµ\8dâµ\9c:",
-       "subject": "â´°âµ\99âµ\8fâµ\9câµ\8d:",
-       "minoredit": "ⵡⴰⴷ âµ\89ⴳⴰ â´°âµ\99âµ\8fâ´¼âµ\8d âµ\93âµ\8eâµ¥âµ\89âµ¢",
+       "summary": "ⴰⵣⴳⵣâµ\8d :",
+       "subject": "âµ\89âµ\8eâµ\94âµ\99âµ\89 :",
+       "minoredit": "â´°âµ\99âµ\8fâ´¼âµ\8d âµ\8eⵥⵥâµ\89âµ\8f",
        "watchthis": "ⴹⴼⵓⵔ ⵜⴰⵙⵏⴰ ⴰⴷ",
-       "savearticle": "Ẓṛig d tḥbut",
+       "savearticle": "ⵣⵎⵎⴻⵎ",
        "preview": "ⴱⵔⵉⴱⵢⵓ",
        "showpreview": "ⵎⵍ ⵜⴰⵎⵓⵖⵍⵉ",
        "showdiff": "ⵎⵍ ⵉⵙⵏⴼⵉⵍⵏ",
        "missingcommenttext": "ⵉ ⵕⴱⴱⵉ ⵙⵙⴽⵛⵎ ⴽⵔⴰ ⵏ ⵓⵖⴼⴰⵡⴰⵍ ⴷⴷⴰⵡ ⴰⵙ.",
        "summary-preview": "ⴱⵔⵉⴱⵢⵓ ⵏ ⵜⵓⴳⵣⵉⵍⵜ ⵏ ⵓⵙⵏⴼⵍ:",
        "blockedtitle": "ⵉⵜⵜⵡⴰⴳⴷⵍ ⵓⵙⵎⵔⴰⵙ ⴰⴷ",
-       "blockednoreason": "âµ\93âµ\94 âµ\9câ´»âµ\9câµ\9câµ¢âµ\93ⴼⴽâµ\89 ⴽⵔⴰ ⵏ ⵜⵎⵏⵜⵉⵍⵜ",
-       "whitelistedittext": "âµ\89 âµ\95ⴱⴱⵉ $1 ⵉⵖ ⵜⵔⵉⵜ ⴰⴷ ⵜⵙⵏⴼⵍⵜ ⵜⴰⵙⵏⵉⵡⵉⵏ.",
+       "blockednoreason": "âµ\93âµ\94 âµ\9câ´»âµ\9câµ\9cⵢⴰⵡⴼⴽⴰ ⴽⵔⴰ ⵏ ⵜⵎⵏⵜⵉⵍⵜ",
+       "whitelistedittext": "âµ\89 âµ\94ⴱⴱⵉ $1 ⵉⵖ ⵜⵔⵉⵜ ⴰⴷ ⵜⵙⵏⴼⵍⵜ ⵜⴰⵙⵏⵉⵡⵉⵏ.",
        "confirmedittext": "Illa fllak ad talst i tansa nk tbratin urta tsbadalt tisniwin.\nKcm zwar tft tansan nk tbratin ɣ [[Special:Preferences|Timssusmin n umqdac]].",
        "nosuchsectiontitle": "Ur as tufit ad taft ayyaw ad.",
        "nosuchsectiontext": "ⵜⵓⵔⵎⵜ ⴰⴷ ⵜⵙⵏⴼⵍⵜ ⵢⴰⵜ ⵜⴳⵣⵎⵉ ⵓⵔ ⵉⵍⵍⵉⵏ.\nⵉⵥⴹⴰⵔ ⴰⴷ ⵜⵉⵍⵉ ⵜⴻⵜⵜⵢⴰⵡⵙⵎⴰⵜⵜⵉ ⵏⵖ ⵜⴻⵜⵜⵡⴰⴽⴽⵙ ⵍⵍⵉⵖ ⴰⵔ ⵜⴻⵜⵜⴰⵏⵏⴰⵢⵜ ⵜⴰⵙⵏⴰ.",
        "page_first": "ⵜⴰⵎⵣⵡⴰⵔⵓⵜ",
        "page_last": "ⵜⴰⵎⴳⴳⴰⵔⵓⵜ",
        "histlegend": "Diff selection: ⵕⵛⵎ the radio boxes ⵏ ⵜⵓⵏⵖⵉⵍⵉⵏ ⵏⵏⴰ ⵜⵔⵉⵜ ⴰⴷ ⵜⵙⵎⵣⴰⵣⴰⵍⵜ, ⵜⴰⴷⴷⵜ ⵖⴼ enter ⵏⵖ ⵜⴰⴱⵓⵟⵓⵏⵜ ⵉⵍⵍⴰⵏ ⴷⴷⴰⵡ ⴰⵙ.<br />\nⵜⵉⵣⴳⵣⵉⵍⵉⵏ: <strong>({{int:cur}})</strong> = ⴰⵎⵣⴰⵔⴰⵢ ⵉⵍⵍⴰⵏ ⴷ ⵜⵓⵏⵖⵉⵍⵜ ⵉⴳⴳⵯⵔⴰⵏ, <strong>({{int:last}})</strong> = ⴰⵎⵣⴰⵔⴰⵢ ⵉⵍⵍⴰⵏ ⴷ ⵜⵓⵏⵖⵉⵍⵜ ⵉⵣⵡⴰⵔⵏ ⵜⴰⴷ, <strong>{{int:minoreditletter}}</strong> = ⴰⵙⵏⴼⵍ ⵓⵎⵥⵉⵢ.",
-       "history-fieldset-title": "ⵙⵉⴳⴳⵍ revisions",
+       "history-fieldset-title": "ⵙⵉⴳⴳⵍ ⵉⵣⵣⵔⴰⵢⵏ",
        "history-show-deleted": "ⵖⴰⵔ ⵜⵓⵏⵖⵉⵍⵜ ⵏⵏⴰ ⵉⵜⵜⵡⴰⴽⴽⵙⵏ",
-       "histfirst": "â´°âµ\87â´·âµ\89âµ\8e â´°â´½â´½âµ¯",
-       "histlast": "â´°âµ\8eâ´°âµ¢âµ\8fâµ\93 â´°â´½â´½âµ¯",
+       "histfirst": "ⴰⴽⴽⵯ âµ\89âµ\87â´·âµ\8eâµ\8f",
+       "histlast": "ⴰⴽⴽⵯ âµ\89âµ\8aâ´·âµ\89â´·âµ\8f",
        "historyempty": "(ⵉⵅⵡⴰ)",
        "history-feed-title": "ⴰⵎⵣⵔⵓⵢ ⵏ ⵜⵓⵏⵖⵉⵍⵉⵏ",
        "history-feed-description": "ⴰⵎⵣⵔⵓⵢ ⵏ ⵜⵓⵏⵖⵉⵍⵉⵏ ⵏ ⵜⴰⵙⵏⴰ ⴰⴷ ⵉⵍⵍⴰⵏ ⵖ ⵓⵡⵉⴽⵉ",
        "history-title": "ⴰⵎⵣⵔⵓⵢ ⵏ \"$1\"",
        "difference-title": "ⴰⵎⵣⴰⵔⴰⵢ ⵉⵍⵍⴰⵏ ⴳⵔ ⵜⵓⵏⵖⵉⵍⵉⵏ ⵏ \"$1\"",
        "difference-multipage": "(ⴰⵎⵣⴰⵔⴰⵢ ⴳⵔ ⵜⴰⵙⵏⵉⵡⵉⵏ)",
-       "lineno": "ⴰⵣⵔⵉⵔⴳ $1:",
+       "lineno": "ⴰⵣⵔⵉⵔⴳ $1 :",
        "compareselectedversions": "ⵙⵎⵣⴰⵣⴰⵍ ⵉⵣⵣⵔⴰⵢⵏ ⵜⵜⵢⴰⵙⵜⴰⵢⵏⵉⵏ",
        "showhideselectedversions": "Ml/Ḥbu ilqmn lli ittuystayn",
        "editundo": "ⵉⵡⵔⵔⵉ ⵏⵏ",
        "searchmenu-exists": "<strong>ⵜⵍⵍⴰ ⵢⴰⵜ ⵜⴰⵙⵏⴰ ⵉⵍⴰⵏ ⵉⵙⵎ ⵏ \"[[:$1]]\" ⵖ ⵓⵡⵉⴽⵉ ⴰⴷ.</strong> {{PLURAL:$2|0=|See also the other search results found.}}",
        "searchmenu-new": "<strong>ⵙⵏⵓⵍⴼⵓ ⵜⴰⵙⵏⴰ \"[[:$1]]\" ⵖ ⵓⵡⵉⴽⵉ ⴰⴷ!</strong> {{PLURAL:$2|0=|See also the page found with your search.|See also the search results found.}}",
        "searchprofile-articles": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⵏ ⵜⵓⵎⴰⵢⵜ",
-       "searchprofile-images": "â´°â´³âµ\9cⵎⵉⴷⵢⴰ",
+       "searchprofile-images": "âµ\8eâµ\93âµ\8dâµ\9câµ\89ⵎⵉⴷⵢⴰ",
        "searchprofile-everything": "ⴰⴽⴽⵯ",
        "searchprofile-advanced": "Advanced",
        "searchprofile-articles-tooltip": "ⵙⵉⴳⴳⵍ ⵖ $1",
        "recentchanges-summary": "ⴹⴼⵓⵔ ⴰⵎⴰⵢⵏⵓ ⴰⴽⴽⵯ ⵖ ⵉⵙⵏⴼⵉⵍⵏ ⵏ ⵓⵡⵉⴽⵉ ⵖ ⵜⴰⵙⵏⴰ ⴰⴷ.",
        "recentchanges-feed-description": "ⴹⴼⵓⵔ ⵉⵙⵏⴼⵉⵍⵏ ⴰⴽⴽⵯ ⵉⴳⴳⵯⵔⴰⵏ ⵏ ⵓⵡⵉⴽⵉ ⵖ ⵉⴼⵉⵍⵉ ⴰⴷ.",
        "recentchanges-label-newpage": "ⵉⵙⵏⵓⵍⴼⴰ ⵓⵙⵏⴼⵍ ⴰⴷ ⵢⴰⵜ ⵜⴰⵙⵏⴰ ⵜⴰⵎⴰⵢⵏⵓⵜ",
-       "recentchanges-label-minor": "ⵡⴰⴷ âµ\89ⴳⴰ â´°âµ\99âµ\8fâ´¼âµ\8d âµ\93âµ\8eâµ¥âµ\89âµ¢",
+       "recentchanges-label-minor": "ⵡⴰⴷ âµ\89ⴳⴰ â´°âµ\99âµ\8fâ´¼âµ\8d âµ\8eⵥⵥâµ\89âµ\8f",
        "recentchanges-label-bot": "ⴰⵙⵏⴼⵍ ⴰⴷ ⵉⵙⴽⵔ ⵜ ⵢⴰⵏ ⵓⵔⵓⴱⵓ",
        "recentchanges-label-unpatrolled": "Ambddl ad ura jju ittmẓra",
        "recentchanges-label-plusminus": "ⵜⵏⴼⵍ ⵜⵉⴷⴷⵉ ⵏ ⵜⴰⵙⵏⴰ ⵙ ⵡⵓⵟⵟⵓⵏ ⴰⴷ ⵏ ⵉⴷ ⴱⴰⵢⵜ",
-       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (âµ¥âµ\94 âµ\93âµ\8dâ´° [[Special:NewPages|âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵜⵉⵎⴰⵢⵏⵓⵜⵉⵏ]])",
+       "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (âµ¥âµ\94 âµ\93âµ\8dâ´° [[Special:NewPages|âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵜⵉⵎⴰⵢⵏⵓⵜⵉⵏ]])",
        "rcfilters-legend-heading": "<strong>ⵜⵉⵣⴳⵣⵉⵍⵉⵏ:</strong>",
        "rcfilters-days-title": "ⵓⵙⵙⴰⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ",
        "rcfilters-hours-title": "ⵜⵉⵙⵔⴰⴳⵉⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ",
        "rcfilters-days-show-days": "$1 {{PLURAL:$1|ⵡⴰⵙⵙ|ⵡⵓⵙⵙⴰⵏ}}",
        "rcfilters-savedqueries-new-name-label": "ⵉⵙⵎ",
        "rcfilters-filterlist-whatsthis": "ⵎⴰⵏⵉⴽ ⵙ ⴰⵔ ⵉⵙⵡⵓⵔⵓⵢ ⵖⴰⵢⴰⴷ?",
+       "rcfilters-filter-editsbyself-description": "ⵜⵉⴷⵔⴰⵡⵉⵏ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}}",
        "rcfilters-filter-bots-label": "ⴰⵔⵓⴱⵓ",
        "rcnotefrom": "ⴷⴷⴰⵡ ⴰⵙ {{PLURAL:$5|ⴰⵙⵏⴼⵍ|ⵉⵙⵏⴼⵉⵍⵏ}} ⵣⵖ <strong>$3, $4</strong> (ⴰⵔ <strong>$1</strong> ⴰⴷ ⵉⴱⴰⵢⵏⴻⵏ ⵙ ⵉⴳⴳⵓⵜ).",
        "rclistfrom": "ⵎⵍ ⵉⵙⵏⴼⵉⵍⵏ ⵉⵎⴰⵢⵏⵓⵜⵏ ⵣⵖ $3 ⵖ $2",
        "newsectionsummary": "/* $1 */ ⵜⵉⴳⵣⵎⵉ ⵜⴰⵎⴰⵢⵏⵓⵜ",
        "rc-enhanced-expand": "ⵙⵎⵍ ⵉⴼⵔⵓⵔⵉⵜⵏ",
        "rc-enhanced-hide": "ⵙⵙⵏⵜⵍ ⵉⴼⵔⵓⵔⵉⵜⵏ",
-       "recentchangeslinked": "ⵉⵙⵏⴼⵉⵍⵏ ⴷⴰⵔ ⴰⵙⵙⴰⵖ",
+       "recentchangeslinked": "âµ\89âµ\99âµ\8fâ´¼âµ\89âµ\8dâµ\8f âµ\96 â´·â´°âµ\94 â´°âµ\99âµ\99â´°âµ\96",
        "recentchangeslinked-feed": "Imbddeln zund ɣwid",
        "recentchangeslinked-toolbox": "Imbddeln zund ɣwid",
        "recentchangeslinked-title": "ⵉⵙⵏⴼⵉⵍⵏ ⵇⵇⵏⵏⵉⵏ ⵙ \"$1\"",
        "recentchangeslinked-summary": "ⵉⵙⵏⴼⵉⵍⵏ ⵜⵜⵢⵓⵙⴽⴰⵔⵏⵉⵏ ⵜⵉⴳⵉⵔⴰ ⴰⴷ ⵉ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵏⵏⴰ ⵙⵔⵙⵏⵜ ⵜⴻⵜⵜⴰⵡⵉ ⴽⵔⴰ ⵏ ⵜⴰⵙⵏⴰ ⵉⵥⵍⵉⵏ (ⵏⵖ ⵉ ⵉⴳⵎⴰⵎⵏ ⵏ ⴽⵔⴰ ⵏ ⵜⴰⴳⴳⴰⵢⵜ ⵉⵥⵍⵉⵏ).\nⵜⴰⵙⵏⵉⵡⵉⵏ ⵍⵍⴰⵏⵉⵏ ⵖ [[Special:Watchlist|ⵜⵍⴳⴰⵎⵜ ⵏⵏⴽ/ⵎ ⵏ ⵓⴹⴼⴼⵓⵔ]] ⵍⵍⴰⵏⵜ ⵙ ⵜⵉⵔⵔⴰ ⵣⵓⵔⵏⵉⵏ.",
-       "recentchangeslinked-page": "ⵉⵙⵎ ⵏ ⵜⴰⵙⵏⴰ:",
+       "recentchangeslinked-page": "ⵉⵙⵎ ⵏ ⵜⴰⵙⵏⴰ :",
        "recentchangeslinked-to": "ⵎⵍ ⵉⵙⵏⴼⵉⵍⵏ ⵏ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵇⵇⵏⵏⵉⵏ ⵙ ⵜⴰⵙⵏⴰ instead",
-       "upload": "âµ\99â´½âµ\9câµ\94 ⴽⵔⴰ ⵏ ⵓⴼⴰⵢⵍⵓ",
+       "upload": "â´°âµ\8dâµ\8d â´· ⴽⵔⴰ ⵏ ⵓⴼⴰⵢⵍⵓ",
        "uploadbtn": "ⵙⴽⵜⵔ ⴰⴼⴰⵢⵍⵓ",
        "reuploaddesc": "Sbidd asrbu d turrit",
        "upload-tryagain": "Ṣafḍ Anglam n ufaylu li ibudln",
        "upload-form-label-usage-filename": "ⵉⵙⵎ ⵏ ⵓⴼⴰⵢⵍⵓ",
        "upload-form-label-infoform-categories": "ⵜⴰⴳⴳⴰⵢⵉⵏ",
        "upload-form-label-infoform-date": "ⴰⵙⴰⴽⵓⴷ",
-       "license": "ⵜⵓⵔⴰⴳⵜ:",
+       "license": "ⵜⵓⵔⴰⴳⵜ :",
        "license-header": "ⵜⵓⵔⴰⴳⵜ",
        "listfiles-delete": "ⴽⴽⵙ",
        "imgfile": "ⴰⴼⴰⵢⵍⵓ",
-       "listfiles": "âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵉⴼⴰⵢⵍⵓⵜⵏ",
+       "listfiles": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵉⴼⴰⵢⵍⵓⵜⵏ",
        "listfiles_date": "ⴰⵙⴰⴽⵓⴷ",
        "listfiles_name": "ⵉⵙⵎ",
        "listfiles_count": "ⵜⵓⵏⵖⵉⵍⵉⵏ",
        "filehist-revert": "ⵙⵙⴰⴹⵓ",
        "filehist-current": "ⴰⵎⵉⵔⴰⵏ",
        "filehist-datetime": "ⴰⵙⴰⴽⵓⴷ/ⴰⴽⵓⴷ",
-       "filehist-thumb": "Awlaf imżżin",
+       "filehist-thumb": "ⵜⴰⵡⵍⴰⴼⵜ ⵎⵥⵥⵉⵏ",
        "filehist-thumbtext": "Mżżi n lqim ɣ tizi $1",
        "filehist-user": "ⴰⵙⵎⵔⴰⵙ",
        "filehist-dimensions": "ⵉⵎⵏⴰⴷⵏ",
        "filehist-comment": "ⴰⵖⴼⴰⵡⴰⵍ",
-       "imagelinks": "â´°âµ\99âµ\87â´·âµ\9b ⵏ ⵓⴼⴰⵢⵍⵓ",
+       "imagelinks": "â´°âµ\99âµ\99âµ\8eâµ\94âµ\99 ⵏ ⵓⴼⴰⵢⵍⵓ",
        "linkstoimage": "{{PLURAL:$1|ⵜⴰⵙⵏⴰ ⴰⴷ ⵉⴹⴼⴰⵔⵏ ⴳⵉⵙ|$1 ⵜⴰⵙⵏⵉⵡⵉⵏ ⴰⴷ ⵉⴹⴼⴰⵔⵏ ⴳⵉⵙⵏⵜ}} ⴰⴼⴰⵢⵍⵓ ⴰⴷ:",
        "nolinkstoimage": "ⵓⵔ ⵜⵍⵍⵉ ⴽⵔⴰ ⵏ ⵜⴰⵙⵏⴰ ⵉⵙⵙⵎⵔⴰⵙⵏ ⴰⴼⴰⵢⵍⵓ ⴰⴷ.",
        "sharedupload": "Asdawad z $1 tẓḍart at tsxdmt gr iswirn yaḍnin",
        "filedelete": "ⴽⴽⵙ $1",
        "filedelete-legend": "ⴽⴽⵙ ⴰⴼⴰⵢⵍⵓ",
        "filedelete-submit": "ⴽⴽⵙ",
-       "randompage": "âµ\9câ´°âµ\99âµ\8fâ´° âµ\9câ´°â´·âµ\80âµ\8eâ´°âµ\99âµ\9c",
+       "randompage": "âµ\9câ´°âµ\99âµ\8fâ´° âµ\8eâ´½âµ\8fâµ\8fâ´° â´½â´° âµ\9cⴳⴰ",
        "randomincategory-category": "ⴰⵙⵎⵉⵍ:",
        "statistics": "ⵜⵉⵙⵉⴹⴰⵏ",
        "statistics-header-edits": "ⵜⵉⵙⵉⴹⴰⵏ ⵏ ⵉⵙⵏⴼⵉⵍⵏ",
        "uncategorizedcategories": "ⵜⴰⴳⴳⴰⵢⵉⵏ ⵓⵔ ⴰⵎⵓⵏⵉⵏ ⵖ ⵜⴰⴳⴳⴰⵢⵉⵏ",
        "prefixindex": "ⴽⵓⵍⵍⵓ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵍⴰⵏⵉⵏ ⴰⵣⵡⵉⵔ",
        "protectedpages-page": "ⵜⴰⵙⵏⴰ",
-       "listusers": "âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵉⵙⵎⵔⴰⵙⵏ",
+       "listusers": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵉⵙⵎⵔⴰⵙⵏ",
        "usercreated": "{{GENDER:$3|ⵉⵏⵓⵍⴼⴰ|ⵜⵏⵓⵍⴼⴰ}} ⵖ $1 ⵖ $2",
        "newpages": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⵜⵉⵎⴰⵢⵏⵓⵜⵉⵏ",
        "move": "ⵙⵎⵓⵜⵜⵉ",
        "allpagesto": "Mel tasniwin li ttgurunin s",
        "allarticles": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⴰⴽⴽⵯ",
        "allinnamespace": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⴰⴽⴽⵯ ($1 namespace)",
-       "allpagessubmit": "Ftu",
+       "allpagessubmit": "ⴼⵜⵓ",
        "allpagesprefix": "ⵙⵎⵍ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵏⵏⴰ ⵢⴰⴷⴷⴰⵏ ⵙ:",
        "categories": "ⵜⴰⴳⴳⴰⵢⵉⵏ",
+       "sp-deletedcontributions-contribs": "ⵜⵉⴷⵔⴰⵡⵉⵏ",
        "linksearch": "ⴰⵔⵣⵣⵓ ⵖ ⵉⵍⵉⵏⴽⵏ ⵉⴱⵕⵕⴰⵏⴻⵏ",
        "linksearch-ok": "ⵙⵉⴳⴳⵍ",
        "linksearch-line": "$1 tmmuttid z $2",
        "listgrouprights-group": "ⵜⴰⵔⴰⴱⴱⵓⵜ",
-       "listgrouprights-members": "(âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵉⴳⵎⴰⵎⵏ)",
+       "listgrouprights-members": "(âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵉⴳⵎⴰⵎⵏ)",
        "emailuser": "Azn tabrat umsqdac ad",
        "emailsubject": "ⴰⵙⵏⵜⵍ:",
        "emailmessage": "ⵜⵓⵣⵉⵏⵜ:",
        "emailsend": "ⴰⵣⵏ",
-       "watchlist": "âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
-       "mywatchlist": "âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
+       "watchlist": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
+       "mywatchlist": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
        "watchlistfor2": "ⵉ $1 $2",
        "addedwatchtext": "tasna « [[:$1]] » tllan ɣ [[Special:Watchlist|umuɣ n umtfr]]. Imbdln lli dyuckan d tasna lli dis iṭṭuzn rad asn nskr agmmaḍ nsn. Tasna radd ttbayan s \"uḍnay\" ɣ [[Special:RecentChanges|Umuɣ n imbddeln imaynutn]]",
        "removedwatchtext": "Tasna \"[[:$1]]\" ḥra ttuykkas z [[Special:Watchlist|your watchlist]].",
        "deletecomment": "ⵜⴰⵎⵏⵜⵉⵍⵜ:",
        "deleteotherreason": "ⵜⴰⵎⵏⵜⵉⵍⵜ ⵢⴰⴹⵏ/:",
        "deletereasonotherlist": "ⵜⴰⵎⵏⵜⵉⵍⵜ ⵢⴰⴹⵏ",
-       "rollbacklink": "Rard",
+       "rollbacklink": "ⵔⴰⵔ ⴷ",
        "changecontentmodel-submit": "ⵙⵏⴼⵍ",
        "protectlogpage": "Iɣmisn n ugdal",
        "protectedarticle": "ay gdl  \"[[$1]]\"",
        "namespace": "Taɣult",
        "invert": "amglb n ustay",
        "blanknamespace": "(ⴰⴷⵙⵍⴰⵏ)",
-       "contributions": "âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ ⵏ {{GENDER:$1|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}}",
-       "contributions-title": "âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ ⵏ {{GENDER:$1|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}} $1",
-       "mycontris": "âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ",
+       "contributions": "âµ\9câµ\89â´·âµ\94ⴰⵡⵉⵏ ⵏ {{GENDER:$1|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}}",
+       "contributions-title": "âµ\9câµ\89â´·âµ\94ⴰⵡⵉⵏ ⵏ {{GENDER:$1|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}} $1",
+       "mycontris": "âµ\9câµ\89â´·âµ\94ⴰⵡⵉⵏ",
        "anoncontribs": "ⵜⵓⵎⵓⵜⵉⵏ",
        "contribsub2": "ⵉ {{GENDER:$3|$1}} ($2)",
        "uctop": "ⵜⴰⵎⵉⵔⴰⵏⵜ",
        "month": "ⵣⵖ ⵡⴰⵢⵢⵓⵔ (ⴷ ⵣⵉⴽⴽ ⵏⵏⵙ):",
        "year": "ⵣⵖ ⵓⵙⴳⴳⵯⴰⵙ (ⴷ ⵣⵉⴽⴽ ⵏⵏⵙ):",
-       "sp-contributions-newbies": "âµ\8eâµ\8d âµ\96â´°âµ\94 âµ\9câµ\93âµ\8eâµ\93âµ\9câµ\89âµ\8f âµ\8f âµ\89âµ\8eâµ\89ⴹⴰâµ\8fâ´»ⵏ ⵉⵎⴰⵢⵏⵓⵜⵏ",
+       "sp-contributions-newbies": "âµ\8eâµ\8d âµ\96â´°âµ\94 âµ\9câµ\89â´·âµ\94ⴰⵡâµ\89âµ\8f âµ\8f âµ\89âµ\8eâµ\89ⴹⴰâµ\8fⵏ ⵉⵎⴰⵢⵏⵓⵜⵏ",
        "sp-contributions-newbies-sub": "Z imiḍan (comptes) imaynutn",
        "sp-contributions-newbies-title": "Tiwuriwin n umqdac z imḍan imaynutn",
        "sp-contributions-blocklog": "Tinɣmas n willi ttuyqqanin (blocage)",
        "sp-contributions-userrights": "Sgiddi izrfan",
        "sp-contributions-blocked-notice": "Amsqdac ad ittuysbddad. Maf ittuysbddad illa ɣ uɣmmis n n willi n sbid. Mayad ɣ trit ad tsnt maɣ",
        "sp-contributions-blocked-notice-anon": "Tansa yad IP ttuysbddad. Maf ittuysbddad illa ɣ uɣmmis n n willi n sbid. Mayad ɣ trit ad tsnt maɣ",
-       "sp-contributions-search": "âµ\99âµ\89ⴳⴳâµ\8d âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ",
+       "sp-contributions-search": "âµ\99âµ\89ⴳⴳâµ\8d âµ\9câµ\89â´·âµ\94ⴰⵡⵉⵏ",
        "sp-contributions-username": "Tansa IP niɣ assaɣ nu umsqdac:",
        "sp-contributions-toponly": "ⵎⵍ ⵖⴰⵔ ⵉⵙⵏⴼⵉⵍⵏ ⴳⴰⵏⵉⵏ ⵜⵓⵏⵖⵉⵍⵉⵏ ⵜⵉⵎⵉⵔⴰⵏⵉⵏ",
        "sp-contributions-newonly": "ⵎⵍ ⵖⴰⵔ ⵉⵙⵏⴼⵉⵍⵏ ⴳⴰⵏⵉⵏ ⵉⵙⵏⵓⵍⴼⵓⵜⵏ ⵏ ⵜⴰⵙⵏⵉⵡⵉⵏ",
        "sp-contributions-submit": "ⵙⵉⴳⴳⵍ",
-       "whatlinkshere": "âµ\8eâ´°â´· âµ\89âµ\9câµ\9cⴰⵡⵢⵏ ⵙ ⵖⵉⴷ",
+       "whatlinkshere": "âµ\8eâ´°â´· âµ\89âµ\9câµ\9cⴰⵡâµ\89ⵏ ⵙ ⵖⵉⴷ",
        "whatlinkshere-title": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⵉⵜⵜⴰⵡⵢⵏ ⵙ \"$1\"",
-       "whatlinkshere-page": "ⵜⴰⵙⵏⴰ:",
+       "whatlinkshere-page": "ⵜⴰⵙⵏⴰ :",
        "linkshere": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⴰⴷ ⴹⴼⴰⵔⵏⵉⵏ ⴰⵔ ⵜⵜⴰⵡⵉⵏⵜ ⵙ <strong>$2</strong>:",
        "nolinkshere": "ⵓⵍⴰ ⴽⵔⴰ ⵏ ⵜⴰⵙⵏⴰ ⵓⵔ ⴰⵔ ⵜⴻⵜⵜⴰⵡⵉ ⵙ <strong>$2</strong>.",
        "nolinkshere-ns": "Ur tlla kra n tasna izdin d  '''$2''' ɣ tɣult l-ittuystayn.",
        "whatlinkshere-hideimages": "$1 ⵉⵍⵉⵏⴽⵏ ⵏ ⵓⴼⴰⵢⵍⵓ",
        "whatlinkshere-filters": "ⵜⵉⵙⵜⵜⴰⵢⵉⵏ",
        "blockip": "ⴳⴷⵍ {{GENDER:$1|ⴰⵙⵎⵔⴰⵙ|ⵜⴰⵙⵎⵔⴰⵙⵜ}}",
-       "ipboptions": "2 ikudn:2 hours,1 as:1 day,3 ussan:3 days,1 imalas:1 week,2 imalasn:2 weeks,1 ayur:1 month,3 irn:3 months,6 irn:6 months,1 asggas:1 year,tusut ur iswuttan:infinite",
+       "ipboptions": "2 ⵏ ⵜⵙⵔⴰⴳⵉⵏ:2 hours,1 ⵏ ⵡⴰⵙⵙ:1 day,3 ⵏ ⵡⵓⵙⵙⴰⵏ:3 days,1 ⵏ ⵉⵎⴰⵍⴰⵙⵙ:1 week,2 ⵏ ⵉⴷ ⵉⵎⴰⵍⴰⵙⵙ:2 weeks,1 ⵏ ⵡⴰⵢⵢⵓ:1 month,3 ⵏ ⵉⵢⵢⵉⵔⵏ:3 months,6 ⵏ ⵉⵢⵢⵉⵔⵏ:6 months,1 ⵏ ⵓⵙⴳⴳⵯⴰⵙ:1 year,ⴱⴷⴷⴰ:infinite",
        "ipbhidename": "ḥbu assaɣ n umsqdac ɣ imbdln d umuɣn",
        "ipbwatchuser": "Tfr tisniwin d imsgdaln n umqdac",
        "autoblocklist-submit": "ⵙⵉⴳⴳⵍ",
        "blocklink": "ⴳⴷⵍ",
        "unblocklink": "ⴽⴽⵙ ⴰⴳⴷⴷⵓⵍ",
        "change-blocklink": "ⵙⵏⴼⵍ ⴰⴳⴷⴷⵓⵍ",
-       "contribslink": "âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ",
+       "contribslink": "âµ\9câµ\89â´·âµ\94ⴰⵡⵉⵏ",
        "blocklogpage": "aɣmmis n may ittuyqqanin",
        "blocklog-showlog": "Amsqdac ikkattin ittuyqqan. anɣmis n willi ttuyqqanin  ɣid:",
        "blocklog-showsuppresslog": "Amsqdac ikkattin ittuyqqan d iḥba. Anɣmis n willi ttuyqqanin  ɣid:",
        "tooltip-pt-anontalk": "Amsgdal f imbddeln n tansa n IP yad",
        "tooltip-pt-preferences": "ⵜⵉⵙⵖⴰⵍ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}}",
        "tooltip-pt-watchlist": "Tifilit n tisnatin li itsaggan imdddeln li gisnt ittyskarn..",
-       "tooltip-pt-mycontris": "âµ\9câ´°âµ\8dⴳⴰâµ\8eâµ\9c âµ\8f âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}}",
+       "tooltip-pt-mycontris": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9câµ\9c âµ\8f âµ\9câ´·âµ\94ⴰⵡⵉⵏ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}}",
        "tooltip-pt-login": "ⵢⵓⴼ ⴰⴽ ⴰ'ⵏⵏ ⵜⵣⴷⵢⵜ, ⵎⴰⵛⴰ ⵓⵔ ⵉⴳⵉ ⴱⵣⵣⵉⵣⵜ.",
        "tooltip-pt-logout": "ⴼⴼⵖ",
        "tooltip-ca-talk": "ⴰⵎⵙⴰⵡⴰⵍ ⴼ ⵜⴰⵙⵏⴰ ⵏ ⵜⵓⵎⴰⵢⵜ",
        "tooltip-ca-undelete": "Rard imbddeln imzwura li ittyskarnin ɣ tasna yad",
        "tooltip-ca-move": "ⵙⵎⵓⵜⵜⵉ ⵜⴰⵙⵏⴰ ⴰⴷ",
        "tooltip-ca-watch": "ⵔⵏⵓ ⵜⴰⵙⵏⴰ ⴰⴷ ⵉ ⵜⵍⴳⴰⵎⵜ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}} ⵏ ⵓⴹⴼⴼⵓⵔ",
-       "tooltip-ca-unwatch": "âµ\99âµ\89âµ\9câµ\9câµ\89 âµ\9câ´°âµ\99âµ\8fâ´° â´°â´· âµ£âµ\96 âµ\9câµ\8dⴳⴰâµ\8eⵜ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}} ⵏ ⵓⴹⴼⴼⵓⵔ",
+       "tooltip-ca-unwatch": "âµ\99âµ\89âµ\9câµ\9câµ\89 âµ\8fâµ\8f âµ\9câ´°âµ\99âµ\8fâ´° â´°â´· âµ£âµ\96 âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ {{GENDER:|ⵏⵏⴽ|ⵏⵏⵎ}} ⵏ ⵓⴹⴼⴼⵓⵔ",
        "tooltip-search": "ⵙⵉⴳⴳⵍ ⵖ {{SITENAME}}",
        "tooltip-search-go": "ⴼⵜⵓ ⵙ ⵜⴰⵙⵏⴰ ⵉⵍⴰⵏ ⵏⵉⵜ ⵉⵙⵎ ⴰⴷ ⵉⵖ ⵜⵍⵍⴰ",
        "tooltip-search-fulltext": "ⵙⵉⴳⴳⵍ ⴰⴹⵔⵉⵙ ⴰⴷ ⵖ ⵜⴰⵙⵏⵉⵡⵉⵏ",
        "tooltip-n-mainpage-description": "ⴽⴽ ⴷ ⵜⴰⵙⵏⴰ ⵏ ⵓⵙⵏⵓⴱⴳ",
        "tooltip-n-portal": "ⴼ ⵓⴱⵕⵓⵊⵉ, ⵎⴰⴷ ⵜⵥⴹⴰⵔⵜ ⴰⴷ ⵜ ⵜⵙⴽⵔⵜ, ⵎⴰⵏⵉ ⵔⴰⴷ ⵜⴰⴼⵜ ⵓⵎⵍⴰⵏ",
        "tooltip-n-currentevents": "Tiɣri izrbn i kullu maɣid immusn",
-       "tooltip-n-recentchanges": "âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⵉⵙⵏⴼⵉⵍⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ ⵖ ⵓⵡⵉⴽⵉ",
+       "tooltip-n-recentchanges": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⵉⵙⵏⴼⵉⵍⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ ⵖ ⵓⵡⵉⴽⵉ",
        "tooltip-n-randompage": "ⵣⴷⵎ ⴷ ⴽⵔⴰ ⵏ ⵜⴰⵙⵏⴰ ⵙ ⵓⴷⵀⵎⴰⵙ",
-       "tooltip-n-help": "Adɣar n w-aws",
-       "tooltip-t-whatlinkshere": "âµ\9câ´°âµ\8dⴳⴰâµ\8eⵜ ⵏ ⴽⵓⵍⵍⵓ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵉⵜⵜⴰⵡⵉⵏ ⵙ ⵖⵉⴷ",
+       "tooltip-n-help": "ⵎⴰ ⵖ 'ⵜⵜⴰⴼⴰⵜ ⵜⵉⵡⵉⵙⵉ",
+       "tooltip-t-whatlinkshere": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9cⵜ ⵏ ⴽⵓⵍⵍⵓ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵉⵜⵜⴰⵡⵉⵏ ⵙ ⵖⵉⴷ",
        "tooltip-t-recentchangeslinked": "ⵉⵙⵏⴼⵉⵍⵏ ⴳⴳⵯⵔⴰⵏⵉⵏ ⵖ ⵜⴰⵙⵏⵉⵡⵉⵏ ⵏⵏⴰ ⵙⵔⵙⵏ ⵜⴻⵜⵜⴰⵡⵉ ⵜⴰⵙⵏⴰ ⴰⴷ",
        "tooltip-feed-rss": "Usuddm (Flux) n tasna yad",
        "tooltip-feed-atom": "ⵉⴼⵉⵍⵉ ⴰⵟⵓⵎ ⵏ ⵜⴰⵙⵏⴰ ⴰⴷ",
-       "tooltip-t-contributions": "âµ\9câ´°âµ\8dⴳⴰâµ\8eâµ\9c âµ\8f âµ\9câµ\93âµ\8eâµ\93âµ\9cⵉⵏ ⵏ {{GENDER:$1|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}} ⴰⴷ",
+       "tooltip-t-contributions": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9câµ\9c âµ\8f âµ\9câ´·âµ\94ⴰⵡⵉⵏ ⵏ {{GENDER:$1|ⵓⵙⵎⵔⴰⵙ|ⵜⵙⵎⵔⴰⵙⵜ}} ⴰⴷ",
        "tooltip-t-emailuser": "Ṣafd tabrat umsqdac ad",
-       "tooltip-t-upload": "âµ\99â´½âµ\9câµ\94 ⵉⴼⴰⵢⵍⵓⵜⵏ",
-       "tooltip-t-specialpages": "âµ\9câ´°âµ\8dⴳⴰâµ\8eâµ\9c âµ\8f â´½âµ\93âµ\8dâµ\8dâµ\93 âµ\9câ´°âµ\99âµ\8fâµ\89ⵡâµ\89âµ\8f âµ¥âµ\8dâµ\89âµ\8fⵉⵏ",
+       "tooltip-t-upload": "â´°âµ\8dâµ\8d â´· ⵉⴼⴰⵢⵍⵓⵜⵏ",
+       "tooltip-t-specialpages": "âµ\9câ´°âµ\8dâµ\89âµ\99âµ\9câµ\9c âµ\8f â´½âµ\93âµ\8dâµ\8dâµ\93 âµ\9câ´°âµ\99âµ\8fâµ\89ⵡâµ\89âµ\8f âµ\89âµ¥âµ\8dⵉⵏ",
        "tooltip-t-print": "Printable version of this page",
-       "tooltip-t-permalink": "Azday bdda i lqim n tasna yad",
+       "tooltip-t-permalink": "ⴰⵍⵉⵏⴽ ⵉⴳⴰⵏ ⵡⵉⵏ ⴱⴷⴷⴰ ⵉ ⵜⵓⵏⵖⵉⵍⵜ ⴰⴷ ⵏ ⵜⴰⵙⵏⴰ",
        "tooltip-ca-nstab-main": "ⵥⵔ ⵜⴰⵙⵏⴰ ⵏ ⵜⵓⵎⴰⵢⵜ",
        "tooltip-ca-nstab-user": "Ẓr tasna n useqdac",
        "tooltip-ca-nstab-media": "Iẓri n tasna n midya",
        "tooltip-upload": "Izwir siɣ tullt.",
        "tooltip-rollback": "\"Rard\" s yan klik ażrig (iżrign) s ɣiklli sttin kkan tiklit li igguran",
        "tooltip-undo": "\"Sglb\" ḥiyd ambdl ad t mmurẓmt tasatmt n umbdl ɣ umuḍ tiẓri tamzwarut.",
-       "tooltip-summary": "Skcm yat tayafut imẓẓin",
+       "tooltip-summary": "ⵙⵙⴽⵛⵎ ⵏⵏ ⴽⵔⴰ ⵏ ⵓⵣⴳⵣⵍ ⵎⵥⵥⵉⵏ",
+       "pageinfo-header-basic": "ⵓⵎⵍⴰⵏ ⵉⵙⵉⵍⴰⵏⵏ",
        "pageinfo-header-edits": "ⴰⵎⵣⵔⵓⵢ ⵏ ⵓⵙⵏⴼⵍ",
        "pageinfo-length": "ⵜⵉⴷⴷⵉ ⵏ ⵜⴰⵙⵏⴰ (ⵙ ⵉⴷ ⴱⴰⵢⵜ)",
        "pageinfo-language": "ⵜⵓⵜⵍⴰⵢⵜ ⵏ ⵜⵓⵎⴰⵢⵜ ⵏ ⵜⴰⵙⵏⴰ",
        "pageinfo-firsttime": "ⴰⵙⴰⴽⵓⴷ ⵏ ⵓⵙⵏⵓⵍⴼⵓ ⵏ ⵜⴰⵙⵏⴰ",
        "pageinfo-lastuser": "ⴰⵎⵙⵏⴼⵍ ⵉⴳⴳⵯⵔⴰⵏ",
        "pageinfo-lasttime": "ⴰⵙⴰⴽⵓⴷ ⵏ ⵓⵙⵏⴼⵍ ⴰⴽⴽⵯ ⵉⴳⴳⵯⵔⴰⵏ",
+       "pageinfo-magic-words": "{{PLURAL:$1|ⵜⴰⴳⵓⵔⵉ ⵉⵙⵎⴰⵊⵉⴽⵉⵏ|ⵜⵉⴳⵓⵔⵉⵡⵉⵏ ⵙⵎⴰⵊⵉⴽⵏⵉⵏ}} ($1)",
        "pageinfo-hidden-categories": "{{PLURAL:$1|ⴰⵙⵎⵉⵍ ⵉⵏⵜⵍⵏ|ⵉⵙⵎⵉⵍⵏ ⵏⵜⵍⵏⵉⵏ}} ($1)",
        "pageinfo-contentpage-yes": "ⵢⴰⵀ",
        "pageinfo-protect-cascading-yes": "ⵢⴰⵀ",
        "confirm-rollback-button": "ⵡⴰⵅⵅⴰ",
        "quotation-marks": "\"$1\"",
        "imgmultipagenext": "ⵜⴰⵙⵏⴰ ⴷ ⵉⴹⴼⴰⵔⵏ →",
-       "imgmultigo": "â´·â´·âµ\93!",
+       "imgmultigo": "âµ\8dâ´½âµ\8e âµ\8fâµ\8f !",
        "imgmultigoto": "ⴼⵜⵓ ⵙ ⵜⴰⵙⵏⴰ $1",
        "img-lang-default": "(ⵜⵓⵜⵍⴰⵢⵜ ⵙ ⵓⵡⵏⵓⵍ)",
        "ascending_abbrev": "aryaqliw",
index 693b350..83851c8 100644 (file)
        "blocklist-nousertalk": "забрањено уређивање сопствене странице за разговор",
        "blocklist-editing": "уређивање",
        "blocklist-editing-sitewide": "уређивање (на целом сајту)",
+       "blocklist-editing-ns": "именски простори",
        "ipblocklist-empty": "Списак блокирања је празан.",
        "ipblocklist-no-results": "Тражена IP адреса или корисничко име није блокирано.",
        "blocklink": "блокирај",
index 5a1c848..6fe42d0 100644 (file)
                ]
        },
        "tog-underline": "Podvlačenje veza:",
-       "tog-hideminor": "Sakrij manje izmene sa spiska skorašnjih izmena",
+       "tog-hideminor": "Sakrivaj manje izmene sa spiska skorašnjih izmena",
        "tog-hidepatrolled": "Sakrij patrolirane izmene sa spiska skorašnjih izmena",
        "tog-newpageshidepatrolled": "Sakrij patrolirane stranice sa spiska novih stranica",
-       "tog-hidecategorization": "Sakrij kategorizaciju stranica",
-       "tog-extendwatchlist": "Proširi spisak nadgledanja za prikaz svih promena, ne samo nedavnih",
+       "tog-hidecategorization": "Sakrivaj kategorizaciju stranica",
+       "tog-extendwatchlist": "Proširuj spisak nadgledanja za prikaz svih promena, ne samo nedavnih",
        "tog-usenewrc": "Grupiši promene po stranici u skorašnjim izmenama i spisku nadgledanja",
        "tog-numberheadings": "Automatski numeriši naslove",
        "tog-editondblclick": "Omogući uređivanje stranica dvostrukim klikom",
        "tog-watchuploads": "Dodaj nove datoteke koje otpremim na moj spisak nadgledanja",
        "tog-watchrollback": "Dodaj stranice na kojima sam izvršio vraćanje izmena na moj spisak nadgledanja",
        "tog-minordefault": "Podrazumevano označavaj sve izmene kao manje",
-       "tog-previewontop": "Prikaži pretpregled pre okvira za uređivanje",
-       "tog-previewonfirst": "Prikaži pretpregled pri prvoj izmeni",
-       "tog-enotifwatchlistpages": "Pošalji mi imejl kada se promeni stranica ili datoteka sa mog spiska nadgledanja",
-       "tog-enotifusertalkpages": "Pošalji mi imejl kad se promeni moja korisnička stranica za razgovor",
-       "tog-enotifminoredits": "Takođe mi pošalji imejl kod manjih izmena stranica i datoteka",
-       "tog-enotifrevealaddr": "Otkrij moju imejl-adresu u imejlovima obaveštenja",
+       "tog-previewontop": "Prikazuj pretpregled pre okvira za uređivanje",
+       "tog-previewonfirst": "Prikazuj pretpregled pri prvoj izmeni",
+       "tog-enotifwatchlistpages": "Pošalji mi e-poruku kada se promeni stranica ili datoteka sa mog spiska nadgledanja",
+       "tog-enotifusertalkpages": "Pošalji mi e-poruku kad se promeni moja korisnička stranica za razgovor",
+       "tog-enotifminoredits": "Pošalji mi e-poruku i kod manjih izmena stranica i datoteka",
+       "tog-enotifrevealaddr": "Otkrij moju adresu e-pošte u e-porukama za obaveštavanje",
        "tog-shownumberswatching": "Prikaži broj korisnika koji nadgledaju",
        "tog-oldsig": "Vaš postojeći potpis:",
        "tog-fancysig": "Smatraj potpis kao vikitekst (bez automatskog povezivanja)",
-       "tog-uselivepreview": "Prikaži pretpregled bez ponovnog učitavanja stranice",
+       "tog-uselivepreview": "Prikazuj pretpregled bez ponovnog učitavanja stranice",
        "tog-forceeditsummary": "Upozori me kada ne unesem opis izmene",
-       "tog-watchlisthideown": "Sakrij moje izmene sa spiska nadgledanja",
-       "tog-watchlisthidebots": "Sakrij izmene botova sa spiska nadgledanja",
-       "tog-watchlisthideminor": "Sakrij manje izmene sa spiska nadgledanja",
-       "tog-watchlisthideliu": "Sakrij izmene prijavljenih korisnika sa spiska nadgledanja",
+       "tog-watchlisthideown": "Sakrivaj moje izmene sa spiska nadgledanja",
+       "tog-watchlisthidebots": "Sakrivaj izmene botova sa spiska nadgledanja",
+       "tog-watchlisthideminor": "Sakrivaj manje izmene sa spiska nadgledanja",
+       "tog-watchlisthideliu": "Sakrivaj izmene prijavljenih korisnika sa spiska nadgledanja",
        "tog-watchlistreloadautomatically": "Automatski ponovo učitaj spisak nadgledanja kad god se filter promeni (potreban JavaScript)",
        "tog-watchlistunwatchlinks": "Dodaj označivače za prekid nadgledanja/nagledanje ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) na nadgledane stranice sa promenama (za funkcionalnost prebacivanja je potreban JavaScript)",
-       "tog-watchlisthideanons": "Sakrij izmene anonimnih korisnika sa spiska nadgledanja",
+       "tog-watchlisthideanons": "Sakrivaj izmene anonimnih korisnika sa spiska nadgledanja",
        "tog-watchlisthidepatrolled": "Sakrij patrolirane izmene sa spiska nadgledanja",
-       "tog-watchlisthidecategorization": "Sakrij kategorizaciju stranica",
-       "tog-ccmeonemails": "Pošalji mi kopije imejlova koje pošaljem drugim korisnicima",
+       "tog-watchlisthidecategorization": "Sakrivaj kategorizaciju stranica",
+       "tog-ccmeonemails": "Pošalji mi kopije e-poruka koje pošaljem drugim korisnicima",
        "tog-diffonly": "Ne prikazuj sadržaj stranice ispod razlika",
-       "tog-showhiddencats": "Prikaži skrivene kategorije",
+       "tog-showhiddencats": "Prikazuj skrivene kategorije",
        "tog-norollbackdiff": "Ne prikazuj razliku nakon izvršenog vraćanja",
        "tog-useeditwarning": "Upozori me kada napuštam stranicu za uređivanje sa nesačuvanim promenama",
        "tog-prefershttps": "Uvek koristi bezbednu vezu dok sam prijavljen/a.",
        "returnto": "Nazad na stranicu „$1”.",
        "tagline": "Izvor: {{SITENAME}}",
        "help": "Pomoć",
+       "help-mediawiki": "Pomoć o MediaWiki-ju",
        "search": "Pretraga",
        "search-ignored-headings": " #<!-- ne menjajte ništa u ovom redu --> <pre>\n# Naslovi koji će biti zanemareni pri pretrazi.\n# Promene su vidljive odmah nakon što se stranica sa naslovom indeksira.\n# Možete iznuditi ponovno indeksiranje „nultom” izmenom.\n# Sintaksa je sledeća:\n#  * Svaki red koji započinje znakom „#” je komentar.\n#  * Svaki ne prazni red je tačan naslov koji će biti zanemaren, s tim da se razlikuju mala i velika slova i sve ostalo\nReference\nSpoljašnje veze\nTakođe pogledajte\n #</pre> <!-- ne menjajte ništa u ovom redu -->",
        "searchbutton": "Pretraži",
        "toolbox": "Alatke",
        "tool-link-userrights": "Promena {{GENDER:$1|korisničkih}} grupa",
        "tool-link-userrights-readonly": "Prikaz {{GENDER:$1|korisničkih}} grupa",
-       "tool-link-emailuser": "Slanje imejla {{GENDER:$1|korisniku|korisnici}}",
+       "tool-link-emailuser": "Slanje e-poruke {{GENDER:$1|korisniku|korisnici|korisniku/ci}}",
        "imagepage": "Prikaži stranicu datoteke",
        "mediawikipage": "Prikaži stranicu poruke",
        "templatepage": "Prikaži stranicu šablona",
        "viewtalkpage": "Prikaži diskusiju",
        "otherlanguages": "Na drugim jezicima",
        "redirectedfrom": "(preusmereno sa $1)",
-       "redirectpagesub": "Preusmeravanje",
+       "redirectpagesub": "Preusmerenje",
        "redirectto": "Preusmerava na:",
        "lastmodifiedat": "Ova stranica je poslednji put uređena na datum $1 u $2 č.",
        "viewcount": "Ovoj stranici je pristupljeno {{PLURAL:$1|jedanput|$1 puta}}.",
        "pool-errorunknown": "Nepoznata greška",
        "pool-servererror": "Usluga brojača redova nije dostupna ($1).",
        "poolcounter-usage-error": "Greška pri upotrebi: $1",
-       "aboutsite": "O {{GRAMMAR:dativ|{{SITENAME}}}}",
-       "aboutpage": "Project:O_{{GRAMMAR:dativ|{{SITENAME}}}}",
+       "aboutsite": "O projektu {{SITENAME}}",
+       "aboutpage": "Project:O_projektu_{{SITENAME}}",
        "copyright": "Sadržaj je dostupan pod licencom $1 osim ako je drugačije navedeno.",
        "copyrightpage": "{{ns:project}}:Autorska prava",
        "currentevents": "Aktuelnosti",
        "protectedpagetext": "Ova stranica je zaključana za izmene i druge radnje.",
        "viewsourcetext": "Možete da vidite i kopirate izvor ove stranice.",
        "viewyourtext": "Možete da vidite i kopirate izvor <strong>vaših izmena</strong> na ovoj stranici.",
-       "protectedinterface": "Ova stranica sadrži tekst interfejsa za softver na ovom vikiju i zaštićena je radi sprečavanja zloupotrebe.\nDa biste dodali ili promenili prevode bilo kojeg vikija, posetite [https://translatewiki.net/ translatewiki.net], projekat za lokalizaciju Medijavikija.",
-       "editinginterface": "<strong>Upozorenje:</strong> uređujete stranicu koja se koristi za prikazivanje teksta korisničkog okruženja.\nIzmene na ovoj stranici će uticati na sve korisnike ovog vikija.",
+       "protectedinterface": "Ova stranica obezbeđuje tekst interfejsa za softver na ovom vikiju i zaštićena je radi sprečavanja zloupotrebe.\nDa biste dodali ili promenili prevode svih vikija, posetite [https://translatewiki.net/ translatewiki.net], projekat za lokalizaciju MediaWiki-ja.",
+       "editinginterface": "<strong>Upozorenje:</strong> Uređujete stranicu koja se koristi za obezbeđivanje teksta interfejsa za softver.\nPromene na ovoj stranici uticaće na izgled korisničkog interfejsa drugih korisnika ovog vikija.",
        "translateinterface": "Da biste dodali ili promenili prevode za sve vikije, posetite [https://translatewiki.net/ translatewiki.net], projekat za lokalizaciju Medijavikija.",
        "cascadeprotected": "Ova stranica je zaključana jer sadrži {{PLURAL:$1|sledeću stranicu koja je zaštićena|sledeće stranice koje su zaštićene}} „prenosivom“ zaštitom:\n$2",
        "namespaceprotected": "Nemate dozvolu da uređujete stranice u imenskom prostoru: <strong>$1</strong>.",
        "userlogout": "Odjava",
        "notloggedin": "Niste prijavljeni",
        "userlogin-noaccount": "Nemate nalog?",
-       "userlogin-joinproject": "Pridružite se {{GRAMMAR:dativ|{{SITENAME}}}}",
+       "userlogin-joinproject": "Pridružite se projektu {{SITENAME}}",
        "createaccount": "Otvaranje naloga",
        "userlogin-resetpassword-link": "Zaboravili ste lozinku?",
        "userlogin-helplink2": "Pomoć pri prijavljivanju",
        "userlogin-loggedin": "Već ste prijavljeni kao {{GENDER:$1|$1}}.\nKoristite donji obrazac da biste se prijavili kao drugi korisnik.",
        "userlogin-reauth": "Morate da se ponovo prijavite da biste verifikovali da ste {{GENDER:$1|$1}}.",
        "userlogin-createanother": "Otvori još jedan nalog",
-       "createacct-emailrequired": "Imejl-adresa",
-       "createacct-emailoptional": "Imejl-adresa (opcionalno)",
-       "createacct-email-ph": "Unesite imejl-adresu",
-       "createacct-another-email-ph": "Unesite imejl-adresu",
-       "createaccountmail": "Koristite privremenu, slučajnu lozinku i pošaljite je na navedenu imejl-adresu",
+       "createacct-emailrequired": "Adresa e-pošte",
+       "createacct-emailoptional": "Adresa e-pošte (opcionalno)",
+       "createacct-email-ph": "Unesite adresu e-pošte",
+       "createacct-another-email-ph": "Unesite adresu e-pošte",
+       "createaccountmail": "Koristi privremenu, nasumičnu lozinku i pošalji je na navedenu adresu e-pošte",
        "createaccountmail-help": "Može se koristiti da se nekome otvori nalog bez saznanja lozinke.",
        "createacct-realname": "Pravo ime (opcionalno)",
        "createacct-reason": "Razlog",
-       "createacct-reason-ph": "Zašto pravite još jedan nalog?",
+       "createacct-reason-ph": "Zašto otvarate još jedan nalog",
        "createacct-reason-help": "Poruka koja se prikazuje u dnevniku otvaranja naloga",
        "createacct-submit": "Otvori nalog",
        "createacct-another-submit": "Otvori nalog",
        "mailmypassword": "Resetuj lozinku",
        "passwordremindertitle": "{{SITENAME}} — privremena lozinka",
        "passwordremindertext": "Neko sa IP adrese $1 je zatražio novu lozinku na vikiju {{SITENAME}} ($4).\nStvorena je privremena lozinka za {{GENDER:$2|korisnika|korisnicu|korisnika}} $2 koja glasi $3.\nUkoliko je ovo vaš zahtev, sada se prijavite i postavite novu lozinku.\nPrivremena lozinka ističe za {{PLURAL:$5|jedan dan|$5 dana}}.\n\nAko je neko drugi zatražio promenu lozinke, ili ste se setili vaše lozinke i ne želite da je menjate, zanemarite ovu poruku.",
-       "noemail": "Ne postoji imejl-adresa za {{GENDER:$1|korisnika|korisnicu}} $1.",
-       "noemailcreate": "Morate da navedete važeću imejl-adresu.",
-       "passwordsent": "Nova lozinka je poslata na imejl-adresu {{GENDER:$1|korisnika|korisnice}} $1.\nPonovo se prijavite nakon što je primite.",
+       "noemail": "{{GENDER:$1|Korisnik „$1” nije naveo|Korisnica „$1” nije navela|Korisnik/ca „$1” nije naveo/la}} adresu e-pošte.",
+       "noemailcreate": "Morate da navedete važeću adresu e-pošte.",
+       "passwordsent": "Nova lozinka je poslata na adresu e-pošte {{GENDER:$1|korisnika|korisnice|korisnika/ce}} $1.\nPonovo se prijavite nakon što je primite.",
        "blocked-mailpassword": "Uređivanje sa vaše IP adrese je blokirano. Radi sprečavanja zloupotrebe, zabranjena je i funkcija vraćanja lozinke sa nje.",
-       "eauthentsent": "Imejl o potvrdi je poslat na navedenu imejl-adresu.\nPre bilo kojih drugih slanja imejlova na nalog, moraćete pratiti uputstva u imejlu da biste potvrdili da je nalog zaista vaš.",
+       "eauthentsent": "E-poruka o potvrdi je poslata na navedenu adresu e-pošte.\nPre bilo kojih drugih slanja e-poruka na nalog, moraćete pratiti uputstva u e-poruci da biste potvrdili da je nalog zaista vaš.",
        "throttled-mailpassword": "Poruka za promenu lozinke je poslata u {{PLURAL:$1|1=poslednjih sat vremena|poslednja $1 sata|poslednjih $1 sati}}.\nDa bismo sprečili zloupotrebu, podsetnik šaljemo samo jednom u roku od {{PLURAL:$1|1=sat vremena|$1 sata|$1 sati}}.",
        "mailerror": "Greška pri slanju poruke: $1",
-       "acct_creation_throttle_hit": "Posetioci ovog vikija koji koriste vašu IP adresu su već otvorili {{PLURAL:$1|1=jedan nalog|$1 naloga}} prethodni $2, što je najveći dozvoljeni broj u tom vremenskom periodu.\nZbog toga posetioci s ove IP adrese trenutno ne mogu otvoriti više naloga.",
-       "emailauthenticated": "Vaša imejl-adresa je potvrđena na dan $2 u $3 č.",
-       "emailnotauthenticated": "Vaša imejl-adresa još nije potvrđena.\nNijedan imejl neće da bude poslat ni u jednom od sledećih slučajeva.",
-       "noemailprefs": "Navedite imejl-adresu u podešavanjima za osposobljavanje ovih funkcija.",
-       "emailconfirmlink": "Potvrdite svoju imejl-adresu",
-       "invalidemailaddress": "Imejl-adresa ne može da bude prihvaćena jer je u nevažećem obliku.\nUnesite ispravnu adresu ili ostavite prazno polje.",
-       "cannotchangeemail": "Na ovom vikiju nije moguće promeniti imejl-adrese naloga.",
-       "emaildisabled": "Ovaj sajt ne može da šalje imejlove.",
+       "acct_creation_throttle_hit": "Posetioci ovog vikija koji koriste vašu IP adresu su otvorili {{PLURAL:$1|1=jedan nalog|$1 naloga}} u prethodnih $2, što je najveća dozvoljena vrednost u ovom vremenskom periodu.\nKao rezultat toga, ti posetioci trenutno ne mogu otvarati više naloga.",
+       "emailauthenticated": "Vaša adresa e-pošte je potvrđena na dan $2 u $3 č.",
+       "emailnotauthenticated": "Vaša adresa e-pošte još nije potvrđena.\nNijedna e-poruka neće biti poslata ni u jednom od sledećih slučajeva.",
+       "noemailprefs": "Navedite adresu e-pošte u podešavanjima za osposobljavanje ovih funkcija.",
+       "emailconfirmlink": "Potvrdite adresu e-pošte",
+       "invalidemailaddress": "Nije moguće prihvatiti adresu e-pošte jer je u nevažećem formatu.\nUnesite dobro formatiranu adresu ili ostavite prazno polje.",
+       "cannotchangeemail": "Adrese e-pošte naloga ne mogu se promeniti na ovom vikiju.",
+       "emaildisabled": "Ova lokacija ne može da šalje e-poruke.",
        "accountcreated": "Nalog je otvoren",
        "accountcreatedtext": "Korisnički nalog [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|talk]]) je otvoren.",
        "createaccount-title": "Otvaranje korisničkog naloga za {{SITENAME}}",
-       "createaccount-text": "Neko je otvorio nalog sa vašom imejl-adresom na projektu {{SITENAME}} ($4) pod imenom „$2“ i sa lozinkom „$3“.\nOdmah trebate da se prijavite i promenite svoju lozinku.\n\nMožete da zanemarite ovu poruku, ako je ovaj nalog otvoren greškom.",
+       "createaccount-text": "Neko je otvorio nalog sa vašom adresom e-pošte na projektu {{SITENAME}} ($4) pod imenom „$2” i sa lozinkom „$3”.\nOdmah trebate da se prijavite i promenite svoju lozinku.\n\nMožete da zanemarite ovu poruku, ako je ovaj nalog otvoren greškom.",
        "login-throttled": "Previše puta ste pokušali da se prijavite.\nSačekajte $1 pre nego što pokušate ponovo.",
        "login-abort-generic": "Neuspešna prijava – prekinuto",
-       "login-migrated-generic": "Vaš nalog je migriran i vaše korisničko više ne postoji na ovom vikiju.",
+       "login-migrated-generic": "Vaš nalog je migriran. Vaše korisničko više ne postoji na ovom vikiju.",
        "loginlanguagelabel": "Jezik: $1",
        "suspicious-userlogout": "Vaš zahtev za odjavu je odbijen jer izgleda da ga je poslao pokvareni pregledač ili keširani proksi.",
        "createacct-another-realname-tip": "Pravo ime je opcionalno.\nAko odaberete da ga navedete, biće korišćeno za pripisivanje vašeg rada.",
        "pt-createaccount": "Otvaranje naloga",
        "pt-userlogout": "Odjavi me",
        "php-mail-error-unknown": "Nepoznata greška u funkciji PHP mail().",
-       "user-mail-no-addy": "Pokušali ste da pošaljete imejl bez imejl-adrese.",
-       "user-mail-no-body": "Pokušano slanje imejla s praznim ili nerazumno kratkim sadržajem.",
+       "user-mail-no-addy": "Pokušali ste da pošaljete e-poruku bez adrese e-pošte.",
+       "user-mail-no-body": "Pokušali ste da pošaljete e-poruku sa praznim ili nerazumno kratkim sadržajem.",
        "changepassword": "Promena lozinke",
        "resetpass_announce": "Da biste završili prijavu, podesite novu lozinku ovde.",
        "resetpass_text": "<!-- Ovde unesite tekst -->",
        "resetpass-submit-cancel": "Otkaži",
        "resetpass-wrong-oldpass": "Nevažeća privremena ili trenutna lozinka.\nMožda ste već promenili lozinku ili ste zahtevali novu privremenu lozinku.",
        "resetpass-recycled": "Uneli ste sadašnju lozinku, da biste promenili lozinku morate uneti novu.",
-       "resetpass-temp-emailed": "Prijavili ste se sa privremenim kodom iz imejla.\nDa biste završili prijavljivanje morate postaviti novu lozinku ovde:",
+       "resetpass-temp-emailed": "Prijavili ste se sa privremenim kodom iz e-poruke.\nDa biste završili prijavljivanje, morate postaviti novu lozinku ovde:",
        "resetpass-temp-password": "Privremena lozinka:",
        "resetpass-abort-generic": "Promenu lozinke je prekinuo dodatak.",
        "resetpass-expired": "Vaša lozinka je istekla. Postavite novu lozinku da biste se prijavili.",
        "resetpass-expired-soft": "Vaša lozinka je istekla i morate je promeniti. Postavite novu lozinku ili kliknite „{{int:authprovider-resetpass-skip-label}}“ da je promenite kasnije.",
+       "resetpass-validity": "Vaša lozinka nije važeća: $1\n\nPostavite novu da biste se prijavili.",
        "resetpass-validity-soft": "Vaša lozinka nije važeća: $1\n\nIzaberite novu odmah ili kliknite na „{{int:authprovider-resetpass-skip-label}}“ da je promenite kasnije.",
        "passwordreset": "Resetovanje lozinke",
-       "passwordreset-text-one": "Popunite ovaj obrazac da biste dobili privremenu lozinku na imejl.",
-       "passwordreset-text-many": "{{PLURAL:$1|Ispunite jedno od polja kako biste dobili privremenu lozinku putem imejla.}}",
+       "passwordreset-text-one": "Popunite ovaj obrazac da biste primili privremenu lozinku putem e-pošte.",
+       "passwordreset-text-many": "{{PLURAL:$1|Ispunite jedno od polja da biste primili privremenu lozinku putem e-pošte.}}",
        "passwordreset-disabled": "Resetovanje lozinke je onemogućeno na ovom vikiju.",
-       "passwordreset-emaildisabled": "Imejl je onemogućen na ovom vikiju.",
+       "passwordreset-emaildisabled": "Funkcija e-pošte je onemogućena na ovom vikiju.",
        "passwordreset-username": "Korisničko ime:",
        "passwordreset-domain": "Domen:",
-       "passwordreset-email": "Imejl-adresa:",
+       "passwordreset-email": "Adresa e-pošte:",
        "passwordreset-emailtitle": "Detalji naloga na vikiju {{SITENAME}}",
-       "passwordreset-emailtext-ip": "Neko (verovatno vi, sa IP adrese $1) zatražio je resetovanje vaše \nlozinke za projekat {{SITENAME}} ($4). Sledeći korisnički {{PLURAL:$3|nalog je povezan|nalozi su povezani}} \nsa ovom imejl adresom:\n\n$2\n\n{{PLURAL:$3|Ova privremena lozinka|Ove privremene lozinke}} će isteći za {{PLURAL:$5|jedan dan|$5 dana}}.\nOdmah trebate da se prijavite i odaberite novu lozinku. Ako je neko drugi napravio ovaj \nzahtev ili ste se setili svoje prvobitne lozinke, a ne \nželite da je promenite, možete da zanemarite ovu poruku i nastavite da koristite svoju staru \nlozinku.",
-       "passwordreset-emailtext-user": "{{GENDER:$1|Korisnik je zatražio|Korisnica je zatražila}} podsetnik o podacima za prijavu na vikiju {{SITENAME}} ($4).\nSledeći {{PLURAL:$3|korisnički nalog je povezan|korisnički nalozi su povezani}} sa ovom imejl-adresom:\n\n$2\n\n{{PLURAL:$3|Privremena lozinka ističe|Privremene lozinke ističu}} za {{PLURAL:$5|jedan dan|$5 dana}}.\nPrijavite se i izaberite novu lozinku. Ako je neko drugi zahtevao ovu radnju ili ste se setili lozinke i ne želite da je menjate, zanemarite ovu poruku.",
+       "passwordreset-emailtext-ip": "Neko (verovatno vi, sa IP adrese $1) zatražio je resetovanje vaše \nlozinke za projekat {{SITENAME}} ($4). Sledeći korisnički {{PLURAL:$3|nalog je povezan|nalozi su povezani}} \nsa ovom adresom e-pošte:\n\n$2\n\n{{PLURAL:$3|Ova privremena lozinka|Ove privremene lozinke}} isteći će za {{PLURAL:$5|jedan dan|$5 dana}}.\nOdmah trebate da se prijavite i odaberite novu lozinku. \nAko je neko drugi napravio ovaj zahtev ili ste se setili \noriginalne lozinke, a ne želite da je promenite, \nmožete da zanemarite ovu poruku i nastavite da \nkoristite svoju staru lozinku.",
+       "passwordreset-emailtext-user": "Korisnik/ca $1 zatražio/la je resetovanje vaše lozinke na projektu {{SITENAME}} ($4).\nSledeći korisnički {{PLURAL:$3|nalog je povezan|nalozi su povezani}} sa ovom adresom e-pošte:\n\n$2\n\n{{PLURAL:$3|Privremena lozinka ističe|Privremene lozinke ističu}} za {{PLURAL:$5|jedan dan|$5 dana}}.\nOdmah trebate da se prijavite i odaberite novu lozinku. \nAko je neko drugi napravio ovaj zahtev ili ste se setili \noriginalne lozinke, a ne želite da je promenite, \nmožete da zanemarite ovu poruku i nastavite da \nkoristite svoju staru lozinku.",
        "passwordreset-emailelement": "Korisničko ime: \n$1\n\nPrivremena lozinka: \n$2",
-       "passwordreset-emailsentemail": "Ako je ova imejl-adresa povezana sa vašim nalogom, onda će imejl o resetovanju lozinke biti poslat.",
-       "passwordreset-emailsentusername": "Ako postoji imejl-adresa povezana sa ovim korisničkim imenom, onda će imejl o resetovanju lozinke biti poslat.",
+       "passwordreset-emailsentemail": "Ako je ova adresa e-pošte povezana sa vašim nalogom, onda će e-poruka o resetovanju lozinke biti poslata.",
+       "passwordreset-emailsentusername": "Ako postoji adresa e-pošte povezana sa ovim korisničkim imenom, onda će e-poruka o resetovanju lozinke biti poslata.",
        "passwordreset-nocaller": "Pozivalac se mora navesti",
        "passwordreset-nosuchcaller": "Pozivalac ne postoji: $1",
        "passwordreset-ignored": "Resetovanje lozinke nije uspelo. Možda poslužilac nije konfigurisan?",
-       "passwordreset-invalidemail": "Nevažeća imejl-adresa",
+       "passwordreset-invalidemail": "Nevažeća adresa e-pošte",
        "passwordreset-nodata": "Korisničko ime i adresa e-pošte nisu navedeni",
-       "changeemail": "Promena ili uklanjanje imejl-adrese",
-       "changeemail-header": "Popunite ovaj obrazac da bi ste promenili vašu imejl-adresu. Ako biste želeli da uklonite povezanost bilo koje imejl-adrese sa vašeg naloga, ostavite prazno polje za novu imejl-adresu kada šaljete obrazac.",
+       "changeemail": "Promena ili uklanjanje adrese e-pošte",
+       "changeemail-header": "Dovršite ovaj obrazac da bi ste promenili adresu e-pošte. Ako biste želeli da uklonite povezanost bilo koje adrese e-pošte sa vašeg naloga, ostavite prazno polje za novu adresu e-pošte kada šaljete obrazac.",
        "changeemail-no-info": "Morate biti prijavljeni da biste pristupili ovoj stranici.",
-       "changeemail-oldemail": "Aktuelna imejl-adresa:",
-       "changeemail-newemail": "Nova imejl-adresa:",
-       "changeemail-newemail-help": "Ovo polje treba da ostavite prazno ako želite da uklonite svoju imejl-adresu. Nećete biti u mogućnosti da resetujete zaboravljenu lozinku i nećete primati imejlove sa ovog vikija ako je imejl-adresa uklonjena.",
+       "changeemail-oldemail": "Trenutna adresa e-pošte:",
+       "changeemail-newemail": "Nova adresa e-pošte:",
+       "changeemail-newemail-help": "Ovo polje treba da ostavite prazno ako želite da uklonite adresu e-pošte. Nećete biti u mogućnosti da resetujete zaboravljenu lozinku i nećete primati e-poruke sa ovog vikija ako je adresa e-pošte uklonjena.",
        "changeemail-none": "(ništa)",
        "changeemail-password": "Vaša lozinka za projekat {{SITENAME}}:",
-       "changeemail-submit": "Promeni imejl",
+       "changeemail-submit": "Promeni e-poštu",
        "changeemail-throttled": "Previše puta ste pokušali da se prijavite.\nMolimo vas da sačekate $1 pre nego što pokušate ponovo.",
-       "changeemail-nochange": "Unesite drugu imejl-adresu.",
+       "changeemail-nochange": "Unesite drugu adresu e-pošte.",
        "resettokens": "Resetovanje tokena",
        "resettokens-text": "Ovde možete da resetujete tokene koji omogućavaju pristup određenim privatnim podacima povezanim sa vašim nalogom.\n\nTrebali biste to uraditi ako ih slučajno podelite sa nekim ili ako je vaš nalog ugrožen.",
        "resettokens-no-tokens": "Nema žetona za resetovanje.",
        "headline_sample": "Tekst naslova",
        "headline_tip": "Podnaslov (nivo 2)",
        "nowiki_sample": "Ovde umetnite neoblikovan tekst",
-       "nowiki_tip": "Zanemari viki-oblikovanje",
+       "nowiki_tip": "Zanemari viki oblikovanje",
        "image_sample": "Primer.jpg",
        "image_tip": "Ugrađivanje datoteke",
        "media_sample": "Primer.ogg",
        "anoneditwarning": "<strong>Upozorenje:</strong> Niste prijavljeni. Ako objavite stranicu, vaša IP adresa će biti javno vidljiva u njenoj istoriji izmena i drugde. Ako se <strong>[$1 prijavite]</strong> ili <strong>[$2 otvorite nalog]</strong>, pored ostalih pogodnosti koje dobijate vaše izmene će biti pripisivane vašem korisničkom imenu.",
        "anonpreviewwarning": "<em>Niste prijavljeni. Ako objavite stranicu, vaša IP adresa će biti javno vidljiva u njenoj istoriji izmena i drugde.</em>",
        "missingsummary": "<strong>Podsetnik:</strong> niste naveli opis izmene.\nAko ponovo kliknete na „$1”, vaša izmena će biti sačuvana bez njega.",
-       "selfredirect": "<strong>Upozorenje:</strong> Preusmeravate ovu stranicu na nju samu.\nMožda ste naveli pogrešnu odredišnu stranicu za preusmeravanje ili uređujete pogrešnu stranicu.\nAko ponovo kliknete na „$1”, preusmeravanje će svejedno biti napravljeno.",
+       "selfredirect": "<strong>Upozorenje:</strong> Preusmeravate ovu stranicu na nju samu.\nMožda ste naveli pogrešnu odredišnu stranicu za preusmerenje ili uređujete pogrešnu stranicu.\nAko ponovo kliknete na „$1”, preusmerenje će svejedno biti napravljeno.",
        "missingcommenttext": "Molimo unesite komentar.",
        "missingcommentheader": "<strong>Napomena:</strong> Niste uneli naslov teme ovog komentara.\nAko ponovo kliknete na „$1”, izmena će biti sačuvana bez naslova.",
        "summary-preview": "Pretpregled opisa izmene:",
        "subject-preview": "Pregled teme:",
        "previewerrortext": "Došlo je do greške pri pokušaju pregleda promena.",
        "blockedtitle": "Korisnik je blokiran",
-       "blocked-email-user": "<strong>Vašem korisničkom imenu je blokirano slanje imejlova. Još uvek možete da uređujete druge stranice na ovom vikiju.</strong> Možete da vidite potpune detalje blokade na [[Special:MyContributions|doprinosima naloga]].\n\nBlokadu je izvršio/la $1.\n\nNaveden je sledeći razlog: <em>$2</em>.\n\n* Početak blokade: $8\n* Istek blokade: $6\n* Namenjena korisniku/ci ili IP adresi: $7\n* ID blokade #$5",
+       "blocked-email-user": "<strong>Vašem korisničkom imenu je blokirano slanje e-poruka. Još uvek možete da uređujete druge stranice na ovom vikiju.</strong> Možete da vidite potpune detalje blokade na [[Special:MyContributions|doprinosima naloga]].\n\nBlokadu je izvršio/la $1.\n\nNaveden je sledeći razlog: <em>$2</em>.\n\n* Početak blokade: $8\n* Istek blokade: $6\n* Namenjena korisniku/ci ili IP adresi: $7\n* ID blokade #$5",
        "blockedtext-partial": "<strong>Vašem korisničkom imenu ili IP adresi je blokirano pravljenje promena na ovoj stranici. Još uvek možete da uređujete druge stranice na ovom vikiju.</strong> Možete da vidite potpune detalje blokade na [[Special:MyContributions|doprinosima naloga]].\n\nBlokadu je izvršio/la $1.\n\nNaveden je sledeći razlog: <em>$2</em>.\n\n* Početak blokade: $8\n* Istek blokade: $6\n* Namenjena korisniku/ci ili IP adresi: $7\n* ID blokade #$5",
        "blockedtext": "<strong>Vaše korisničko ime ili IP adresa je blokirana.</strong>\n\nBlokiranje je {{GENDER:$4|izvršio|izvršila}} $1.\nRazlog je <em>$2</em>.\n\n* Početak blokiranja: $8\n* Istek blokiranja: $6\n* Blokirani: $7\n\nMožete da se obratite {{GENDER:$4|korisniku|korisnici}} $1 ili [[{{MediaWiki:Grouppage-sysop}}|administratoru]] radi diskusije o blokiranju.\nNe možete da koristite mogućnost „{{int:emailuser}}” osim ako ste uneli validnu imejl adresu u svojim [[Special:Preferences|podešavanjima]] naloga i niste blokirani od korišćenja iste.\nVaša trenutna IP adresa je $3, a ID blokiranja #$5.\nNavedite sve informacije odozgo pri stvaranju bilo kakvih upita.",
-       "autoblockedtext": "Vaša IP adresa je automatski blokirana jer ju je koristio drugi korisnik, koga je {{GENDER:$4|blokirao|blokirala}} $1.\nRazlog:\n\n:<em>$2</em>\n\n* Početak blokade: $8\n* Kraj blokade: $6\n* Ime korisnika: $7\n\nMožete da kontaktirate {{GENDER:$4|korisnika|korisnicu}} $1 ili drugog [[{{MediaWiki:Grouppage-sysop}}|administratora]] da biste raspravljali o blokadi.\n\nZapamtite da ne možete da koristite funkciju „{{int:emailuser}}“ osim ako ste naveli važeću imejl-adresu u svojim [[Special:Preferences|podešavanjima]].\n\nVaša trenutna IP adresa je $3, a ID blokade $5.\nUključite sve gornje detalje pri pravljenju bilo kakvih upita.",
+       "autoblockedtext": "Vaša IP adresa je automatski blokirana jer ju je koristio drugi korisnik, koga je {{GENDER:$4|blokirao|blokirala|blokirao/la}} $1.\nRazlog:\n\n:<em>$2</em>\n\n* Početak blokade: $8\n* Kraj blokade: $6\n* Ime korisnika: $7\n\nMožete da kontaktirate {{GENDER:$4|korisnika|korisnicu|korisnika/cu}} $1 ili drugog [[{{MediaWiki:Grouppage-sysop}}|administratora]] da biste raspravljali o blokadi.\n\nZapamtite da ne možete da koristite funkciju „{{int:emailuser}}“ osim ako ste naveli važeću adresu e-pošte u [[Special:Preferences|podešavanjima]].\n\nVaša trenutna IP adresa je $3, a ID blokade $5.\nUključite sve gornje detalje pri pravljenju bilo kakvih upita.",
+       "actionblockedtext": "Blokirano vam je izvršavanje ove radnje.",
        "blockednoreason": "razlog nije naveden",
        "whitelistedittext": "$1 da biste uređivali stranice.",
-       "confirmedittext": "Morate da potvrdite svoju imejl adresu pre uređivanja stranica.\nPostavite i potvrdite imejl adresu preko [[Special:Preferences|podešavanja]].",
+       "confirmedittext": "Morate da potvrdite adresu e-pošte pre uređivanja stranica.\nPostavite i proverite valjanost adrese preko [[Special:Preferences|podešavanja]].",
        "nosuchsectiontitle": "Nije moguće pronaći odeljak",
        "nosuchsectiontext": "Pokušali ste da uredite odeljak koji ne postoji.\nMožda je premešten ili izbrisan dok ste pregledali stranicu.",
        "loginreqtitle": "Potrebna je prijava",
        "edit-gone-missing": "Nije moguće ažurirati stranicu.\nIzgleda da je izbrisana.",
        "edit-conflict": "Sukob izmena.",
        "edit-no-change": "Vaša izmena je zanemarena jer nije bilo nikakvih promena u tekstu.",
+       "edit-slots-cannot-add": "{{PLURAL:$1|Sledeći slot ovde nije poržan|Sledeći slotovi ovde nisu podržani}}: $2.",
+       "edit-slots-cannot-remove": "{{PLURAL:$1|Sledeći slot je obavezan i ne može da se ukloni|Sledeći slotovi su obavezni i ne mogu da se uklone}}: $2.",
+       "edit-slots-missing": "{{PLURAL:$1|Sledeći slot nedostaje|Sledeći slotovi nedostaju}}: $2.",
        "postedit-confirmation-created": "Stranica je napravljena.",
        "postedit-confirmation-restored": "Stranica je vraćena.",
        "postedit-confirmation-saved": "Vaša izmena je sačuvana.",
        "defaultmessagetext": "Podrazumevani tekst poruke",
        "content-failed-to-parse": "Raščlanjivanje sadržaja tipa $2 za model $1 nije uspelo: $3",
        "invalid-content-data": "Nevažeći podaci sadržaja",
-       "content-not-allowed-here": "Sadržaj modela „$1“ nije dozvoljen na stranici [[:$2]]",
+       "content-not-allowed-here": "Sadržaj $1 nije dozvoljen na stranici [[:$2]] u slotu „$3”",
        "editwarning-warning": "Ako napustite ovu stranicu, izgubićete sve izmene koje ste napravili. Ako ste prijavljeni, možete onemogućiti ovo upozorenje u svojim podešavanjima, u odeljku „{{int:prefs-editing}}“.",
        "editpage-invalidcontentmodel-title": "Model sadržaja nije podržan",
        "editpage-invalidcontentmodel-text": "Model sadržaja „$1“ nije podržan.",
        "editpage-notsupportedcontentformat-title": "Format sadržaja nije podržan",
        "editpage-notsupportedcontentformat-text": "Format sadržaja $1 nije podržan za model sadržaja $2.",
-       "content-model-wikitext": "vikitekst",
-       "content-model-text": "čist tekst",
-       "content-model-javascript": "JavaScript",
-       "content-model-css": "CSS",
-       "content-model-json": "JSON",
+       "slot-name-main": "Glavni",
+       "content-model-wikitext": "vikiteksta",
+       "content-model-text": "čistog teksta",
+       "content-model-javascript": "JavaScript-a",
+       "content-model-css": "CSS-a",
+       "content-model-json": "JSON-a",
        "content-json-empty-object": "Prazan objekat",
        "content-json-empty-array": "Prazan niz",
        "deprecated-self-close-category": "Stranice koje koriste nevalidne samozatvarajuće HTML tagove",
        "parser-unstrip-loop-warning": "Utvrđena je petlja",
        "unstrip-depth-warning": "Prekoračen limit Unstrip rekurzije ($1)",
        "converter-manual-rule-error": "Pronađena je greška u pravilu za ručno pretvaranje jezika",
-       "undo-success": "Izmena se može poništiti.\nProverite razlike ispod, pa sačuvajte izmene.",
+       "undo-success": "Izmena može da se opozove.\nProverite poređenje ispod da biste verifikovali da je ovo ono što želite da uradite, a zatim sačuvajte promene ispod da biste završili opoziv izmena.",
        "undo-failure": "Ova izmena se ne može poništiti zbog sukoba izmena.",
        "undo-norev": "Nije moguće vratiti izmenu jer ne postoji ili je izbrisana.",
        "undo-nochange": "Izgleda da je izmena već poništena.",
        "prefs-watchlist-managetokens": "Upravljanje tokenima",
        "prefs-misc": "Razno",
        "prefs-resetpass": "Promena lozinke",
-       "prefs-changeemail": "Promena ili uklanjanje imejl-adrese",
-       "prefs-setemail": "Postavljanje imejl-adrese",
-       "prefs-email": "Opcije imejla",
+       "prefs-changeemail": "Promena ili uklanjanje adrese e-pošte",
+       "prefs-setemail": "Postavljanje adrese e-pošte",
+       "prefs-email": "Opcije e-pošte",
        "prefs-rendering": "Izgled",
        "saveprefs": "Sačuvaj",
        "restoreprefs": "Vrati sva podešavanja na podrazumevane vrednosti (u svim odeljcima)",
        "timezoneregion-europe": "Evropa",
        "timezoneregion-indian": "Indijski okean",
        "timezoneregion-pacific": "Tihi okean",
-       "allowemail": "Dozvoli primanje imejlova od drugih korisnika",
-       "email-allow-new-users-label": "Dozvoli primanje imejlova od novajlija",
-       "email-blacklist-label": "Zabrani primanje imejlova od sledećih korisnika:",
+       "allowemail": "Dozvoli drugim korisnicima da mi šalju e-poruke",
+       "email-allow-new-users-label": "Dozvoli primanje e-poruka od novajlija",
+       "email-blacklist-label": "Zabrani ovim korisnicima da mi šalju e-poruke:",
        "prefs-searchoptions": "Pretraga",
        "prefs-namespaces": "Imenski prostori",
        "default": "podrazumevano",
        "prefs-custom-js": "prilagođeni JavaScript",
        "prefs-common-config": "Deljeni CSS/JSON/JavaScript za sve teme:",
        "prefs-reset-intro": "Možete koristiti ovu stranicu da ponovo postavite svoja podešavanja na podrazumevane vrednosti sajta.\nOvo se ne može opozvati.",
-       "prefs-emailconfirm-label": "Potvrda imejla:",
-       "youremail": "Imejl:",
+       "prefs-emailconfirm-label": "Potvrda e-pošte:",
+       "youremail": "E-pošta:",
        "username": "{{GENDER:$1|Korisničko ime}}:",
        "prefs-memberingroups": "{{GENDER:$2|Član|Članica}} {{PLURAL:$1|grupe|grupa}}:",
        "prefs-memberingroups-type": "$1",
        "gender-male": "On uređuje viki stranice",
        "gender-female": "Ona uređuje viki stranice",
        "prefs-help-gender": "Postavljanje ovog podešavanja je opcionalno.\nSoftver koristi datu vrednost da bi vam se obratio i spomenuo vas drugima koristeći odgovarajući gramatički rod.\nOva informacija će biti javna.",
-       "email": "Imejl",
+       "email": "E-pošta",
        "prefs-help-realname": "Pravo ime je opcionalno.\nAko je navedeno, biće korišćeno za pripisivanje vašeg rada.",
-       "prefs-help-email": "Imejl adresa je opcionalna, ali je potrebna za resetovanje lozinke, ako je zaboravite.",
-       "prefs-help-email-others": "Takođe možete izabrati da dopustite drugima da vas kontaktiraju preko imejla putem veze na vašoj korisničkoj stranici ili stranici za razgovor.\nVaša imejl adresa neće biti prikazana drugim korisnicima koji vas kontaktiraju.",
-       "prefs-help-email-required": "Potrebna je imejl-adresa.",
+       "prefs-help-email": "Adresa e-pošte je opcionalna, ali je potrebna za resetovanje lozinke, ako je zaboravite.",
+       "prefs-help-email-others": "Takođe možete oabrati da dopustite drugima da vas kontaktiraju preko e-pošte putem veze na vašoj korisničkoj stranici ili stranici za razgovor.\nVaša adresa e-pošte neće biti prikazana drugim korisnicima koji vas kontaktiraju.",
+       "prefs-help-email-required": "Potrebna je adresa e-pošte.",
        "prefs-info": "Osnovne informacije",
        "prefs-i18n": "Internacionalizacija",
        "prefs-signature": "Potpis",
        "right-viewsuppressed": "pregledanje izmena skrivenih od svih korisnika",
        "right-suppressionlog": "pregledanje privatnih dnevnika",
        "right-block": "blokiranje daljih izmena drugih korisnika",
-       "right-blockemail": "blokiranje korisnika da šalju imejl",
+       "right-blockemail": "blokiranje funkcije slanja e-poruka korisniku",
        "right-hideuser": "blokiranje korisničkog imena i njegovo sakrivanje od javnosti",
        "right-ipblock-exempt": "zaobilaženje IP blokada, autoblokada i blokada opsega",
        "right-unblockself": "deblokiranje samog sebe",
        "right-editmyuserjs": "uređivanje sopstvenih JavaScript datoteka",
        "right-viewmywatchlist": "pregled sopstvenog spiska nadgledanja",
        "right-editmywatchlist": "uređivanje sopstvenog spiska nadgledanja; neke preduzete radnje će svejedno dodati stranice na spisak i bez ovog prava",
-       "right-viewmyprivateinfo": "pregled svojih privatnih podataka (npr. imejl-adresu, pravo ime)",
-       "right-editmyprivateinfo": "uređivanje sopstvenih privatnih podataka (npr. imejl-adrese, pravog imena)",
+       "right-viewmyprivateinfo": "pregled sopstvenih privatnih podataka (npr. adrese e-pošte, pravo ime)",
+       "right-editmyprivateinfo": "uređivanje sopstvenih privatnih podataka (npr. adrese e-pošte, pravog imena)",
        "right-editmyoptions": "uređivanje sopstvenih podešavanja",
        "right-rollback": "brzo vraćanje izmena poslednjeg korisnika koji je menjao određenu stranicu",
        "right-markbotedits": "označavanje vraćenih izmena kao izmene bota",
        "right-noratelimit": "otpornost na ograničenja",
        "right-import": "uvoženje stranica iz drugih vikija",
        "right-importupload": "uvoženje stranica iz otpremljene datoteke",
-       "right-patrol": "označavanje tuđih izmena patroliranim",
-       "right-autopatrol": "automatsko označavanje izmena patroliranim",
-       "right-patrolmarks": "pregledanje oznaka za patroliranje unutar skorašnjih izmena",
+       "right-patrol": "označavanje izmena drugih korisnika patroliranim",
+       "right-autopatrol": "automatsko označavanje sopstvenih izmena patroliranim",
+       "right-patrolmarks": "pregled oznaka za patroliranje u skorašnjim izmenama",
        "right-unwatchedpages": "pregledanje spiska nenadgledanih stranica",
        "right-mergehistory": "objedinjavanje istorija stranica",
        "right-userrights": "uređivanje svih korisničkih prava",
        "right-userrights-interwiki": "uređivanje korisničkih prava na drugim vikijima",
        "right-siteadmin": "zaključavanje i otključavanje baze podataka",
        "right-override-export-depth": "izvoz stranica uključujući i povazene stranice do dubine od pet veza",
-       "right-sendemail": "slanje imejla drugim korisnicima",
+       "right-sendemail": "slanje e-poruka drugim korisnicima",
        "right-managechangetags": "pravljenje i (de)aktiviranje [[Special:Tags|oznaka]]",
        "right-applychangetags": "primenjivanje [[Special:Tags|oznaka]] na nečije promene",
        "right-changetags": "dodavanje i uklanjanje raznih [[Special:Tags|oznaka]] na pojedinačnim izmenama i unosima u dnevnicima",
        "grant-group-page-interaction": "Uređivanje stranica",
        "grant-group-file-interaction": "Interakcija sa medijima",
        "grant-group-watchlist-interaction": "Uređivanje vašeg spiska nadgledanja",
-       "grant-group-email": "Pošalji imejl",
+       "grant-group-email": "Pošalji e-poruku",
        "grant-group-high-volume": "Izvršavanje velikog broja radnji",
        "grant-group-customization": "Prilagođavanje i podešavanja",
        "grant-group-administration": "Izvršavanje administrativnih radnji",
        "grant-delete": "Brisanje stranica, izmena i unosa u dnevnicima",
        "grant-editinterface": "Uređivanje imenskog prostora Medijaviki i JSON-a sajta/korisnika",
        "grant-editmycssjs": "Uređivanje vašeg CSS/JSON/Javaskripta",
-       "grant-editmyoptions": "Uređivanje vaših korisničkih podešavanja",
+       "grant-editmyoptions": "Uređivanje vaših korisničkih podešavanja i JSON konfiguracije",
        "grant-editmywatchlist": "Uređivanje vašeg spiska nadgledanja",
        "grant-editpage": "Uređivanje postojećih stranica",
        "grant-editprotected": "Uređivanje zaštićenih stranica",
        "grant-privateinfo": "Pristupi privatnim informacijama",
        "grant-protect": "Zaključavanje i otključavanje stranica",
        "grant-rollback": "Vraćanje promena na stranicama",
-       "grant-sendemail": "Slanje imejlova drugim korisnicima",
+       "grant-sendemail": "Slanje e-poruka drugim korisnicima",
        "grant-uploadeditmovefile": "Otpremanje, zamena i premeštanje datoteka",
        "grant-uploadfile": "Otpremanje novih datoteka",
        "grant-basic": "Osnovna prava",
        "action-edit": "uređujete ovu stranicu",
        "action-createpage": "napravite ovu stranicu",
        "action-createtalk": "napravite ovu stranicu za razgovor",
-       "action-createaccount": "napravite ovaj korisnički nalog",
+       "action-createaccount": "otvorite ovaj korisnički nalog",
        "action-autocreateaccount": "automatski napravite ovaj spoljašnji korisnički nalog",
        "action-history": "gledate istoriju ove stranice",
        "action-minoredit": "označite ovu izmenu kao manju",
        "action-rollback": "brzo vratite izmene poslednjeg korisnika koji je uređivao određenu stranicu",
        "action-import": "uvozite stranice iz drugog vikija",
        "action-importupload": "uvozite stranice putem otpremanja datoteke",
-       "action-patrol": "označite tuđe izmene kao patrolirane",
-       "action-autopatrol": "označite sopstvene izmene kao patrolirane",
+       "action-patrol": "označite izmene drugih korisnika patroliranim",
+       "action-autopatrol": "označite sopstvene izmene patroliranim",
        "action-unwatchedpages": "pregledate spisak nenadgledanih stranica",
        "action-mergehistory": "objedinite istoriju ove stranice",
        "action-userrights": "uređujete sva korisnička prava",
        "action-userrights-interwiki": "uređujete korisnička prava korisnika na drugim vikijima",
        "action-siteadmin": "zaključavate ili otključavate bazu podataka",
-       "action-sendemail": "šaljete imejlove",
+       "action-sendemail": "šaljete e-poruke",
        "action-editmyoptions": "uređujete sopstvena podešavanja",
        "action-editmywatchlist": "uređujete sopstveni spisak nadgledanja",
        "action-viewmywatchlist": "pregledate sopstveni spisak nadgledanja",
        "recentchanges-timeout": "Ova pretraga je istekla. Možda želite da pokušate drugačije parametre pretrage.",
        "recentchanges-network": "Zbog tehničkog problema, nije moguće učitati rezultate. Pokušajte da osvežite stranicu.",
        "recentchanges-notargetpage": "Unesite ime stranice iznad da biste videli promene srodne s ovom stranicom",
-       "recentchanges-feed-description": "Pratite najskorije promene na vikiju u ovom fidu.",
+       "recentchanges-feed-description": "Pratite nedavne promene na vikiju u ovom fidu.",
        "recentchanges-label-newpage": "Nova stranica",
        "recentchanges-label-minor": "Manja izmena",
        "recentchanges-label-bot": "Botovska izmena",
        "rcfilters-activefilters": "Aktivni filteri",
        "rcfilters-activefilters-hide": "Sakrij",
        "rcfilters-activefilters-show": "Prikaži",
-       "rcfilters-activefilters-hide-tooltip": "Sakrij oblast aktivnih filtera",
-       "rcfilters-activefilters-show-tooltip": "Prikaži oblast aktivnih filtera",
+       "rcfilters-activefilters-hide-tooltip": "Sakrijte oblast aktivnih filtera",
+       "rcfilters-activefilters-show-tooltip": "Prikažite oblast aktivnih filtera",
        "rcfilters-advancedfilters": "Napredni filteri",
        "rcfilters-limit-title": "Broj izmena za prikaz",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|promena|promene|promena}}, $2",
        "rcfilters-watchlist-edit-watchlist-button": "Uredi spisak nadgledanih stranica",
        "rcfilters-watchlist-showupdated": "Promene na stranicama koje niste posetili od kada je izmena izvršena su <strong>podebljane</strong>, s ispunjenim oznakama.",
        "rcfilters-preference-label": "Koristi interfejs bez JavaScript-a",
-       "rcfilters-preference-help": "Učitava skorašnje izmene bez filtera ili funkcionalnosti isticanja.",
+       "rcfilters-preference-help": "Učitava skorašnje izmene bez pretrage filtera ili funkcionalnosti isticanja.",
        "rcfilters-watchlist-preference-label": "Koristi interfejs bez JavaScript-a",
-       "rcfilters-watchlist-preference-help": "Učitava spisak nadgledanja bez filtera ili funkcionalnosti isticanja.",
+       "rcfilters-watchlist-preference-help": "Učitava spisak nadgledanja bez pretrage filtera ili funkcionalnosti isticanja.",
        "rcfilters-filter-showlinkedfrom-label": "Prikaži promene na stranicama sa kojih dolaze veze",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Stranice sa kojih dolaze veze do</strong> izabrane stranice",
        "rcfilters-filter-showlinkedto-label": "Prikaži promene na stranicama ka kojima vode veze",
        "upload-options": "Opcije otpremanja",
        "watchthisupload": "Nadgledaj ovu datoteku",
        "filewasdeleted": "Datoteka sa ovim imenom je ranije optremljena i nakon toga izbrisana.\nTrebate da proverite $1 pre nego što nastavite sa njenim ponovnim optremanjem.",
-       "filename-thumb-name": "Ovo izgleda kao naziv sličice. Molimo vas da ne otpremate sličice na isti viki. U suprotnom, molimo vas, popravite ime datoteke tako da je korisnije i nema prefiks sličice.",
+       "filename-thumb-name": "Ovo izgleda kao naslov sličice. Ne otpremajte sličice na isti viki. U suprotnom, popravite ime datoteke tako da je korisnije i nema prefiks sličice.",
        "filename-bad-prefix": "Naziv datoteke koju šaljete počinje sa <strong>„$1“</strong>, a njega obično dodeljuju digitalni fotoaparati.\nIzaberite naziv datoteke koji opisuje njen sadržaj.",
        "filename-prefix-blacklist": " #<!-- ostavite ovaj red onakvim kakav jeste --> <pre>\n# Sintaksa je sledeća:\n#   * Sve od tarabe pa do kraja reda je komentar\n#   * Svaki red označava prefiks tipičnih naziva datoteka koje dodeljivaju digitalni aparati\nCIMG # Kasio\nDSC_ # Nikon\nDSCF # Fudži\nDSCN # Nikon\nDUW # neki mobilni telefoni\nIMG # opšte\nJD # Dženoptik\nMGP # Pentaks\nPICT # razno\n #</pre> <!-- ostavite ovaj red onakvim kakav jeste -->",
        "upload-proto-error": "Nevažeći protokol",
        "upload-too-many-redirects": "Adresa sadrži previše preusmerenja",
        "upload-http-error": "Došlo je do HTTP greške: $1",
        "upload-copy-upload-invalid-domain": "Primerci otpremanja nisu dostupni na ovom domenu.",
-       "upload-dialog-disabled": "Postavljanje datoteka pomoću ovog dijaloga je onemogućeno na ovom vikiju.",
+       "upload-dialog-disabled": "Otpremanja datoteka korišćenjem ovog dijaloga su onemogućena na ovom vikiju.",
        "upload-dialog-title": "Otpremanje datoteke",
        "upload-dialog-button-cancel": "Otkaži",
        "upload-dialog-button-back": "Nazad",
        "lockmanager-fail-closelock": "Nije moguće zatvoriti katanac za „$1”.",
        "lockmanager-fail-deletelock": "Nije moguće izbrisati katanac za „$1”.",
        "lockmanager-fail-acquirelock": "Nije moguće steći katanac za „$1”.",
-       "lockmanager-fail-openlock": "Ne mogu da otvorim katanac za „$1“. Uverite se da je vaš direktorijum za otpremanje ispravno konfigurisan i da vaš veb-server ima dozvolu da piše u tom direktorijumu. Pogledajte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUploadDirectory za više informacija.",
+       "lockmanager-fail-openlock": "Nije moguće otvoriti katanac za „$1”. Uverite se da je vaš direktorijum za otpremanje ispravno konfigurisan i da vaš veb-server ima dozvolu da upisuje u tom direktorijumu. Pogledajte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:$wgUploadDirectory za više informacija.",
        "lockmanager-fail-releaselock": "Nije moguće osloboditi katanac za „$1”.",
        "lockmanager-fail-db-bucket": "Nije moguće kontaktirati sa dovoljno katanaca u kanti $1.",
        "lockmanager-fail-db-release": "Nije moguće osloboditi katance u bazi podataka $1.",
        "zip-bad": "Datoteka je oštećena ili je nečitljiva ZIP datoteka.\nBezbednosna provera ne može da se izvrši kako treba.",
        "zip-unsupported": "Datoteka je formata ZIP koji koristi funkcije ZIP koje Medijaviki ne podržava.\nNe može se pravilno proveriti u vezi bezbednosti.",
        "uploadstash": "Otpremanje niza datoteka",
-       "uploadstash-summary": "Ova stranica pruža pristup datotekama koje su otpremljene ili se otpremaju, ali još nisu objavljene. Ove datoteke nisu vidljive nikome, osim korisniku koji ih je otpremio.",
+       "uploadstash-summary": "Ova stranica pruža pristup datotekama koje su otpremljene ili su u procesu otpremanja, ali još nisu objavljene na viki. Ove datoteke nisu vidljive nikome, osim korisniku koji ih je otpremio.",
        "uploadstash-clear": "Obriši niz datoteka",
        "uploadstash-nofiles": "Nemate sakrivene datoteke.",
        "uploadstash-badtoken": "Izvršavanje ove radnje nije uspelo, razlog tome može biti istek vremena za uređivanje. Pokušajte ponovo.",
        "linkstoimage-more": "Više od $1 {{PLURAL:$1|stranica koristi|stranice koriste|stranica koristi}} ovu datoteku.\nSledeći spisak prikazuje {{PLURAL:$1|prvu stranicu koja koristi|prve $1 stranice koje koriste|prvih $1 stranica koje koriste}} samo ovu datoteku.\nDostupan je i [[Special:WhatLinksHere/$2|potpuni spisak]].",
        "nolinkstoimage": "Nema stranica koje koriste ovu datoteku.",
        "morelinkstoimage": "Pogledajte [[Special:WhatLinksHere/$1|više veza]] do ove datoteke.",
-       "linkstoimage-redirect": "$1 (preusmeravanje datoteke) $2",
+       "linkstoimage-redirect": "$1 (preusmerenje datoteke) $2",
        "duplicatesoffile": "{{PLURAL:$1|Sledeća datoteka je duplikat|Sledeće $1 datoteke su duplikati|Sledećih $1 datoteka su duplikati}} ove datoteke ([[Special:FileDuplicateSearch/$2|detaljnije]]):",
        "sharedupload": "Ova datoteka se nalazi na $1 i može se koristiti i na drugim projektima.",
        "sharedupload-desc-there": "Ova datoteka se nalazi na $1 i može se koristiti i na drugim projektima.\nPogledajte [$2 stranicu za opis datoteke] za više detalja o njoj.",
        "mimetype": "MIME tip:",
        "download": "preuzmi",
        "unwatchedpages": "Nenadgledane stranice",
-       "listredirects": "Spisak preusmeravanja",
+       "listredirects": "Spisak preusmerenja",
        "listduplicatedfiles": "Spisak datoteka sa duplikatima",
        "listduplicatedfiles-summary": "Ovo je spisak datoteka koje su duplikat nekih drugih datoteka. Samo lokalne datoteke su prikazane.",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] ima [[$3|{{PLURAL:$2|jedan duplikat|$2 duplikata}}]].",
        "randomincategory-category": "Kategorija:",
        "randomincategory-legend": "Slučajna stranica u kategoriji",
        "randomincategory-submit": "Idi",
-       "randomredirect": "Slučajno preusmeravanje",
+       "randomredirect": "Slučajno preusmerenje",
        "randomredirect-nopages": "Nema preusmerenja u imenskom prostoru „$1“.",
        "statistics": "Statistike",
        "statistics-header-pages": "Stranice",
        "pageswithprop-submit": "Idi",
        "pageswithprop-prophidden-long": "sakriveno dugo tekstualno svojstvo ($1)",
        "pageswithprop-prophidden-binary": "sakriveno dugo binarno svojstvo ($1)",
-       "doubleredirects": "Dvostruka preusmeravanja",
-       "doubleredirectstext": "Ova stranica navodi stranice koje preusmeravaju na druga preusmeravanja.\nSvaki red sadrži veze prema prvom i drugom preusmeravanju, kao i odredišnu stranicu drugog preusmerenja koja je obično „pravi” članak na koga prvo preusmeravanje treba da upućuje.\n<del>Precrtani</del> unosi su već rešeni.",
+       "doubleredirects": "Dvostruka preusmerenja",
+       "doubleredirectstext": "Ova stranica navodi stranice koje preusmeravaju na druga preusmerenja.\nSvaki red sadrži veze prema prvom i drugom preusmerenju, kao i odredišnu stranicu drugog preusmerenja koja je obično „pravi” članak na koga prvo preusmerenje treba da upućuje.\n<del>Precrtani</del> unosi su već rešeni.",
        "double-redirect-fixed-move": "[[$1]] je premešten.\nAutomatski je ažurirano i sada preusmerava na [[$2]].",
        "double-redirect-fixed-maintenance": "Automatski ispravlja dvostruka preusmerenja iz [[$1]] u [[$2]] kao deo održavanja",
        "double-redirect-fixer": "Ispravljač preusmerenja",
-       "brokenredirects": "Pokvarena preusmeravanja",
+       "brokenredirects": "Pokvarena preusmerenja",
        "brokenredirectstext": "Sledeća preusmerenja vode na nepostojeće stranice:",
        "brokenredirects-edit": "uredi",
        "brokenredirects-delete": "izbriši",
        "trackingcategories-nodesc": "Opis nije dostupan.",
        "trackingcategories-disabled": "Kategorija je onemogućena",
        "mailnologin": "Nema adrese za slanje",
-       "mailnologintext": "Morate biti [[Special:UserLogin|prijavljeni]] i imati valjanu imejl adresu u [[Special:Preferences|podešavanjima]] da biste slali imejlove drugim korisnicima.",
-       "emailuser": "Pošalji imejl ovom korisniku/ci",
-       "emailuser-title-target": "Slanje imejla {{GENDER:$1|korisniku|korisnici}}",
+       "mailnologintext": "Morate da se [[Special:UserLogin|prijavite]] i imate važeći adresu e-pošte u [[Special:Preferences|podešavanjima]] da biste slali e-poruke drugim korisnicima.",
+       "emailuser": "Pošalji e-poruku ovom korisniku/ci",
+       "emailuser-title-target": "Slanje e-poruke {{GENDER:$1|korisniku|korisnici|korisniku/ci}}",
        "emailuser-title-notarget": "Slanje imejla korisniku",
        "emailpagetext": "Možete da koristite donji obrazac da pošaljete imejl {{GENDER:$1|ovom korisniku|ovoj korisnici}}.\nImejl koji ste uneli u vašim [[Special:Preferences|podešavanjima]] će se prikazati u polju „Od“, tako da će primalac moći da vam odgovori direktno.",
-       "defemailsubject": "{{SITENAME}} — Imejl od {{GENDER:$1|korisnika|korisnice}} „$1”",
-       "usermaildisabled": "Korisnički imejl je onemogućen",
-       "usermaildisabledtext": "Ne možete da šaljete imejlove drugim korisnicima na ovom vikiju",
-       "noemailtitle": "Nema imejl-adrese",
-       "noemailtext": "Ovaj korisnik nije naveo važeću imejl-adresu.",
-       "nowikiemailtext": "Ovaj korisnik je odlučio da ne prima imejlove od drugih korisnika.",
+       "defemailsubject": "{{SITENAME}} — e-poruka od {{GENDER:$1|korisnika|korisnice|korisnika/ce}} „$1”",
+       "usermaildisabled": "Korisnička e-pošta je onemogućena",
+       "usermaildisabledtext": "Ne možete da šaljete e-poruke drugim korisnicima na ovom vikiju",
+       "noemailtitle": "Nema adrese e-pošte",
+       "noemailtext": "Ovaj korisnik nije naveo važeću adresu e-pošte.",
+       "nowikiemailtext": "Ovaj korisnik je odabrao da ne prima e-poruke od drugih korisnika.",
        "emailnotarget": "Nepostojeće ili navažeće korisničko ime primaoca.",
        "emailtarget": "Unos korisničkog imena primaoca",
        "emailusername": "Korisničko ime:",
        "emailusernamesubmit": "Pošalji",
-       "email-legend": "Slanje imejla drugom korisniku projekta {{SITENAME}}",
+       "email-legend": "Slanje e-poruke korisniku/ci projekta {{SITENAME}}",
        "emailfrom": "Od:",
        "emailto": "Za:",
        "emailsubject": "Tema:",
        "emailmessage": "Poruka:",
        "emailsend": "Pošalji",
-       "emailccme": "Pošalji mi kopiju poruke na moj imejl.",
+       "emailccme": "Pošalji mi e-poruku sa kopijom moje poruke.",
        "emailccsubject": "Kopija poruke korisniku/ci $1: $2",
-       "emailsent": "Imejl je poslat",
-       "emailsenttext": "Vaša imejl poruka je poslata.",
-       "emailuserfooter": "Ovaj imejl je {{GENDER:$1|poslao|poslala}} $1 {{GENDER:$2|korisniku|korisnici}} $2 pomoću opcije „{{int:emailuser}}“ na vikiju {{SITENAME}}. Ako odgovorite na ovaj imejl, {{GENDER:$2|Vaš}} imejl će biti neposredno prosleđen ka {{GENDER:$1|prvobitnom pošiljaocu}}, čime ćete {{GENDER:$2|mu|joj}} otkriti {{GENDER:$2|svoju}} imejl adresu.",
+       "emailsent": "E-poruka je poslata",
+       "emailsenttext": "Vaša e-poruka je poslata.",
+       "emailuserfooter": "Ovu e-poruku je {{GENDER:$1|poslao|poslala|poslao/la}} $1 {{GENDER:$2|korisniku|korisnici|korisniku/ci}} $2 pomoću opcije „{{int:emailuser}}” na projektu {{SITENAME}}. Ako odgovorite na ovu e-poruku, {{GENDER:$2|vaša}} e-poruka biće neposredno prosleđena ka {{GENDER:$1|originalnom pošiljaocu}}, čime ćete {{GENDER:$2|mu|joj}} otkriti {{GENDER:$2|adresu e-pošte}}.",
        "usermessage-summary": "Slanje sistemske poruke.",
        "usermessage-editor": "Uređivač sistemskih poruka",
        "usermessage-template": "MediaWiki:UserMessage",
        "nowatchlist": "Nemate ništa na svom spisku nadgledanja.",
        "watchlistanontext": "Prijavite se da biste videli ili uređivali stavke na svom spisku nadgledanja.",
        "watchnologin": "Niste prijavljeni",
-       "addwatch": "Dodaj na spisak nadgledanja",
+       "addwatch": "Dodavanje na spisak nadgledanja",
        "addedwatchtext": "Stranica „[[:$1]]“ i njena stranica za razgovor je dodata na vaš [[Special:Watchlist|spisak nadgledanja]].",
        "addedwatchtext-talk": "Stranica „[[:$1]]” i njena pridružena stranica je dodata na vaš [[Special:Watchlist|spisak nadgledanja]]",
        "addedwatchtext-short": "Stranica „$1“ je dodata na vaš spisak nadgledanja.",
        "notanarticle": "Nije stranica sa sadržajem",
        "notvisiblerev": "Poslednja izmena drugog korisnika je izbrisana.",
        "watchlist-details": "Imate {{PLURAL:$1|$1 stranicu|$1 stranice|$1 stranica}} na svom spisku nadgledanja (plus stranice za razgovor).",
-       "wlheader-enotif": "Obaveštenje imejlom je omogućeno.",
+       "wlheader-enotif": "Obaveštavanje e-porukom je omogućeno.",
        "wlheader-showupdated": "Stranice koje su promenjene otkad ste ih poslednji put posetili su <strong>podebljane</strong>.",
        "wlnote": "Ispod {{PLURAL:$1|je poslednja promena|su poslednje <strong>$1</strong> promene|je poslednjih <strong>$1</strong> promena}} u {{PLURAL:$2|prethodnom satu|prethodna <strong>$2</strong> sata|prethodnih <strong>$2</strong> sati}}, zaključno sa $3, $4.",
        "wlshowlast": "Prikaži poslednjih $1 sati, $2 dana",
        "deletecomment": "Razlog:",
        "deleteotherreason": "Drugi/dodatni razlog:",
        "deletereasonotherlist": "Drugi razlog",
-       "deletereason-dropdown": "* Uobičajeni razlozi za brisanje\n** Nepoželjan sadržaj\n** Vandalizam\n** Kršenje autorskih prava\n** Zahtev autora\n** Prekinuto preusmeravanje",
+       "deletereason-dropdown": "* Uobičajeni razlozi za brisanje\n** Nepoželjan sadržaj\n** Vandalizam\n** Kršenje autorskih prava\n** Zahtev autora\n** Prekinuto preusmerenje",
        "delete-edit-reasonlist": "Uredi razloge brisanja",
        "delete-toobig": "Ova stranica ima veliku istoriju izmena, preko $1 {{PLURAL:$1|izmena|izmene|izmena}}.\nBrisanje takvih stranica je ograničeno da bi se sprečilo slučajno opterećenje servera.",
        "delete-warning-toobig": "Ova stranica ima veliku istoriju izmena, preko $1 {{PLURAL:$1|izmena|izmene|izmena}}.\nNjeno brisanje može da poremeti bazu podataka, stoga postupajte s oprezom.",
        "undelete-revision-row2": "$1 ($2) $3 . . $4 $5 $6 $7 $8",
        "namespace": "Imenski prostor:",
        "invert": "Obrni izbor",
-       "tooltip-invert": "Označite ovu kutijucu da biste sakrili promene na stranicana u izabranom imenskom prostoru (i povezanim imenskim prostorima, ako je označeno)",
-       "tooltip-whatlinkshere-invert": "Označite ovu kutijicu za sakrivanje veza sa stranica u izabranom imenskom prostoru.",
+       "tooltip-invert": "Označite ovo polje da biste sakrili promene na stranicama u izabranom imenskom prostoru (i povezanim imenskim prostorima, ako je označeno)",
+       "tooltip-whatlinkshere-invert": "Označite ovo polje da biste sakrili veze sa stranica unutar izabranog imenskog prostora.",
        "namespace_association": "Povezani imenski prostor",
-       "tooltip-namespace_association": "Označite ovu kutijicu da biste uključili i razgovor ili imenski prostor teme koja je povezana sa izabranim imenskim prostorom",
+       "tooltip-namespace_association": "Označite ovo polje da biste uključili razgovor ili imenski prostor teme koja je povezana sa izabranim imenskim prostorom",
        "blanknamespace": "(glavni)",
        "contributions": "{{GENDER:$1|Doprinosi korisnika|Doprinosi korisnice|Korisnički doprinosi}}",
        "contributions-title": "Doprinosi {{GENDER:$1|korisnika|korisnice}} $1",
        "linkshere": "Sledeće stranice vode na stranicu <strong>$2</strong>:",
        "nolinkshere": "Nijedna stranica nije povezana sa: <strong>$2</strong>.",
        "nolinkshere-ns": "Nijedna stranica ne vodi na stranicu <strong>$2</strong> u izabranom imenskom prostoru.",
-       "isredirect": "preusmeravanje",
+       "isredirect": "preusmerenje",
        "istemplate": "uključivanje",
        "isimage": "veza do datoteke",
        "whatlinkshere-prev": "{{PLURAL:$1|prethodni|prethodna $1|prethodnih $1}}",
        "ipbreason": "Razlog:",
        "ipbreason-dropdown": "*Najčešći razlozi za blokiranje\n** Umetanje lažnih informacija\n** Uklanjanje sadržaja sa stranica\n** Dodavanje nepoželjnih veza do spoljašnjih sajtova\n** Unošenje besmislica/grafita u stranice\n** Nepristojno ponašanje\n** Upotreba više naloga\n** Neprihvatljivo korisničko ime",
        "ipb-hardblock": "Spreči prijavljene korisnike da uređuju s ove IP adrese",
-       "ipbcreateaccount": "Onemogući otvaranje naloga",
-       "ipbemailban": "Spreči korisnika da šalje imejlove",
+       "ipbcreateaccount": "Otvaranje naloga",
+       "ipbemailban": "Slanje e-poruka",
        "ipbenableautoblock": "Automatski blokiraj poslednju IP adresu ovog korisnika i sve daljnje adrese s kojih pokuša da uređuje",
        "ipbsubmit": "Blokiraj ovog korisnika",
        "ipbother": "Drugo vreme:",
        "ipboptions": "2 sata:2 hours,1 dan:1 day,3 dana:3 days,1 nedelja:1 week,2 nedelje:2 weeks,1 mesec:1 month,3 meseca:3 months,6 meseci:6 months,1 godina:1 year,trajno:infinite",
        "ipbhidename": "Sakrij korisničko ime sa izmena i spiskova",
        "ipbwatchuser": "Nadgledaj korisničke stranice i stranice za razgovor ovog korisnika",
-       "ipb-disableusertalk": "Onemogući korisniku da uređuje svoju stranicu za razgovor",
+       "ipb-disableusertalk": "Uređivanje sopstvene stranice za razgovor",
        "ipb-change-block": "Ponovno blokiraj korisnika s ovim podešavanjima",
        "ipb-confirm": "Potvrdi blokiranje",
        "ipb-sitewide": "Na celom sajtu",
        "ipb-partial": "Delimično",
        "ipb-pages-label": "Stranice",
+       "ipb-namespaces-label": "Imenski prostori",
        "badipaddress": "Nevažeća IP adresa",
        "blockipsuccesssub": "Blokiranje je uspelo",
        "blockipsuccesstext": "[[Special:Contributions/$1|$1]] je {{GENDER:$1|blokiran|blokirana}}.<br />\nPogledajte [[Special:BlockList|spisak]] za pregled blokada.",
        "ipb-blocklist": "Prikaži postojeće blokade",
        "ipb-blocklist-contribs": "Doprinosi za {{GENDER:$1|$1}}",
        "ipb-blocklist-duration-left": "preostalo: $1",
-       "block-expiry": "Ističe:",
+       "block-actions": "Radnje za blokiranje:",
+       "block-expiry": "Istek:",
+       "block-options": "Dodatne opcije:",
+       "block-prevent-edit": "Uređivanje",
+       "block-reason": "Razlog:",
+       "block-target": "Korisničko ime ili IP adresa",
        "unblockip": "Deblokiranje korisnika",
        "unblockiptext": "Koristite donji obrazac da biste vratili pravo pisanja ranije blokiranoj IP adresi ili korisničkom imenu.",
        "ipusubmit": "Ukloni ovu blokadu",
        "anononlyblock": "samo anonimni",
        "noautoblockblock": "automatsko blokiranje je onemogućeno",
        "createaccountblock": "otvaranje naloga je onemogućeno",
-       "emailblock": "imejl je onemogućen",
+       "emailblock": "e-pošta je onemogućena",
        "blocklist-nousertalk": "zabranjeno uređivanje sopstvene stranice za razgovor",
        "blocklist-editing": "uređivanje",
        "blocklist-editing-sitewide": "uređivanje (na celom sajtu)",
        "unblocklink": "deblokiraj",
        "change-blocklink": "promeni blokadu",
        "contribslink": "doprinosi",
-       "emaillink": "pošalji imejl",
+       "emaillink": "pošalji e-poruku",
        "autoblocker": "Automatski ste blokirani jer delite IP adresu s korisnikom/com [[User:$1|$1]].\nRazlog blokiranja korisnika/ce $1 je „$2“",
        "blocklogpage": "Dnevnik blokiranja",
        "blocklog-showlog": "{{GENDER:$1|Ovaj korisnik je ranije blokiran|Ova korisnica je ranije blokirana}}.\nDnevnik blokiranja je naveden ispod kao referenca:",
        "block-log-flags-anononly": "samo anonimni korisnici",
        "block-log-flags-nocreate": "onemogućeno otvaranje naloga",
        "block-log-flags-noautoblock": "automatsko blokiranje je onemogućeno",
-       "block-log-flags-noemail": "imejl je onemogućen",
+       "block-log-flags-noemail": "e-pošta je onemogućena",
        "block-log-flags-nousertalk": "zabranjeno uređivanje sopstvene stranice za razgovor",
        "block-log-flags-angry-autoblock": "prošireno automatsko blokiranje je omogućeno",
        "block-log-flags-hiddenname": "korisničko ime je sakriveno",
        "unlockconfirm": "Želim da otključam bazu.",
        "lockbtn": "Zaključaj bazu",
        "unlockbtn": "Otključaj bazu",
-       "locknoconfirm": "Niste proverili potvrdnu kutiju.",
+       "locknoconfirm": "Niste potvrdili polje za potvrdu.",
        "lockdbsuccesssub": "Baza je zaključana",
        "unlockdbsuccesssub": "Baza je otključana",
        "lockdbsuccesstext": "Baza podataka je zaključana.<br />\nSetite se da je [[Special:UnlockDB|otključate]] kada završite s održavanjem.",
        "lockedbyandtime": "(od $1 dana $2 u $3)",
        "move-page": "Premeštanje stranice „$1”",
        "move-page-legend": "Premeštanje stranice",
-       "movepagetext": "Korišćenje obrasca ispod preimenovaće stranicu, premeštajući celu njenu istoriju na novo ime.\nStari naslov postaće preusmeravanje na novi.\nAutomatski možete ažurirati preusmeravanja koja vode na originalni naslov.\nAko se odlučite da ne želite, obavezno proverite da li postoje [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|pokvarena]] preusmeravanja.\nOdgovorni ste da osigurate da veze nastave da vode tamo gde treba.\n\nZapamtite da stranica <strong>neće</strong> biti premeštena ako već postoji stranica na novom naslovu, osim ako je ova druga preusmeravanje i nema raniju istoriju izmena.\nTo znači da možete da preimenujete stranicu natrag odakle je preimenovana ako napravite grešku, ali ne možete prepisati postojeću stranicu.\n\n<strong>Napomena:</strong>\nOvo može predstavljati drastičnu i neočekivanu promenu za popularnu stranicu;\nbudite sigurni da razumete posledice ovoga pre nego što nastavite.",
-       "movepagetext-noredirectfixer": "Korišćenje obrasca ispod preimenovaće stranicu, premeštajući celu njenu istoriju na novo ime.\nStari naslov postaće preusmeravanje na novi.\nObavezno proverite da li postoje [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|pokvarena]] preusmeravanja.\nOdgovorni ste da osigurate da veze nastave da vode tamo gde treba.\n\nZapamtite da stranica <strong>neće</strong> biti premeštena ako već postoji stranica na novom naslovu, osim ako je ona preusmeravanje i nema raniju istoriju izmena.\nTo znači da možete da preimenujete stranicu natrag odakle je preimenovana ako napravite grešku, ali ne možete prepisati postojeću stranicu.\n\n<strong>Napomena:</strong>\nOvo može predstavljati drastičnu i neočekivanu promenu za popularnu stranicu;\nbudite sigurni da razumete posledice ovoga pre nego što nastavite.",
+       "movepagetext": "Korišćenje obrasca ispod preimenovaće stranicu, premeštajući celu njenu istoriju na novo ime.\nStari naslov postaće preusmerenje na novi.\nAutomatski možete ažurirati preusmerenja koja vode na originalni naslov.\nAko se odlučite da ne želite, obavezno proverite da li postoje [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|pokvarena]] preusmerenja.\nOdgovorni ste da osigurate da veze nastave da vode tamo gde treba.\n\nZapamtite da stranica <strong>neće</strong> biti premeštena ako već postoji stranica na novom naslovu, osim ako je ova druga preusmerenje i nema raniju istoriju izmena.\nTo znači da možete da preimenujete stranicu natrag odakle je preimenovana ako napravite grešku, ali ne možete prepisati postojeću stranicu.\n\n<strong>Napomena:</strong>\nOvo može predstavljati drastičnu i neočekivanu promenu za popularnu stranicu;\nbudite sigurni da razumete posledice ovoga pre nego što nastavite.",
+       "movepagetext-noredirectfixer": "Korišćenje obrasca ispod preimenovaće stranicu, premeštajući celu njenu istoriju na novo ime.\nStari naslov postaće preusmerenje na novi.\nObavezno proverite da li postoje [[Special:DoubleRedirects|dvostruka]] ili [[Special:BrokenRedirects|pokvarena]] preusmerenja.\nOdgovorni ste da osigurate da veze nastave da vode tamo gde treba.\n\nZapamtite da stranica <strong>neće</strong> biti premeštena ako već postoji stranica na novom naslovu, osim ako je ona preusmerenje i nema raniju istoriju izmena.\nTo znači da možete da preimenujete stranicu natrag odakle je preimenovana ako napravite grešku, ali ne možete prepisati postojeću stranicu.\n\n<strong>Napomena:</strong>\nOvo može predstavljati drastičnu i neočekivanu promenu za popularnu stranicu;\nbudite sigurni da razumete posledice ovoga pre nego što nastavite.",
        "movepagetalktext": "Ako potvrdite izbor u ovom polju za potvrdu, povezana stranica za razgovor biće automatski premeštena na novi naslov, osim ako ovde već postoji stranica za razgovor sa istim naslovom koja nije prazna.\n\nU tom slučaju, moraćete ručno da je premestite ili objedinite, ako ima potrebe za tim.",
        "moveuserpage-warning": "'''Upozorenje:''' na putu ste da premestite korisničku stranicu. Imajte u vidu da će samo stranica biti premeštena, a sam korisnik ''neće'' biti preimenovan.",
        "movecategorypage-warning": "<strong>Upozorenje:</strong> premeštate stranicu kategorije. Imajte na umu da će samo stranica biti premeštena i da sve stranice u staroj kategoriji <em>neće</em> biti rekategorisane u novu kategoriju.",
        "movepagebtn": "Premesti stranicu",
        "pagemovedsub": "Uspešno premeštanje",
        "movepage-moved": "<strong>Stranica „$1“ je premeštena na naslov „$2“</strong>",
-       "movepage-moved-redirect": "Preusmeravanje je napravljeno.",
+       "movepage-moved-redirect": "Preusmerenje je napravljeno.",
        "movepage-moved-noredirect": "Stvaranje preusmerenja je onemogućeno.",
        "movepage-delete-first": "Ciljna stranica ima previše izmena za brisanje kao deo premeštanja stranice.  Prvo ručno izbrišite stranicu, pa pokušajte ponovo.",
        "articleexists": "Stranica sa tim imenom već postoji ili ime koje ste odabrali nije važeće.\nOdaberite drugo.",
        "immobile-target-namespace-iw": "Međuviki veza nije važeće odredište za premeštanje stranice.",
        "immobile-source-page": "Ova stranica se ne može premestiti.",
        "immobile-target-page": "Premeštanje nije moguće na odredišni naslov.",
-       "bad-target-model": "Željeno odredište koristi drugačiji model sadržaja. Ne mogu da pretvorim iz $1 u $2.",
+       "bad-target-model": "Željeno odredište koristi drugi model sadržaja. Nije moguće konvertovati iz $1 u sadržaj $2.",
        "imagenocrossnamespace": "Datoteka se ne može premestiti u imenski prostor koji ne pripada datotekama.",
        "nonfile-cannot-move-to-file": "Ne-datoteke ne možete premestiti u imenski prostor za datoteke",
        "imagetypemismatch": "Proširenje nove datoteke se ne poklapa s njenim tipom.",
        "imageinvalidfilename": "Ciljano ime datoteke je nevažeće",
        "fix-double-redirects": "Ažurirajte sva preusmerenja koja vode do prvobitnog naslova",
-       "move-leave-redirect": "Ostavi preusmeravanje",
+       "move-leave-redirect": "Ostavi preusmerenje",
        "protectedpagemovewarning": "'''Upozorenje:''' Ova stranica je zaštićena, tako da samo korisnici sa administratorskim ovlašćenjima mogu da je premeste.\nNajnoviji unos u dnevniku je naveden ispod kao referenca:",
        "semiprotectedpagemovewarning": "<strong>Napomena:</strong> Ova stranica je zaštićena, tako da samo automatski potvrđeni korisnici mogu da je premeste.\nNajnoviji unos u dnevniku je naveden ispod kao referenca:",
        "move-over-sharedrepo": "[[:$1]] se nalazi na deljenom skladištu. Ako premestite datoteku na ovaj naslov, to će zameniti deljenu datoteku.",
        "tooltip-feed-rss": "RSS fid za ovu stranicu",
        "tooltip-feed-atom": "Atom fid za ovu stranicu",
        "tooltip-t-contributions": "Spisak doprinosa {{GENDER:$1|ovog korisnika|ove korisnice|ovog korisnika}}",
-       "tooltip-t-emailuser": "Pošaljite imejl {{GENDER:$1|ovom korisniku|ovoj korisnici}}",
+       "tooltip-t-emailuser": "Pošaljite e-poruku {{GENDER:$1|ovom korisniku|ovoj korisnici|korisniku/ci}}",
        "tooltip-t-info": "Više informacija o ovoj stranici",
        "tooltip-t-upload": "Otpremite datoteke",
        "tooltip-t-specialpages": "Spisak svih posebnih stranica",
        "tooltip-recreate": "Ponovo napravite stranicu iako je već izbrisana",
        "tooltip-upload": "Započnite otpremanje",
        "tooltip-rollback": "„Vrati“ vraća izmene poslednjeg doprinosioca ove stranice jednim klikom",
-       "tooltip-undo": "„Poništi” vraća ovu izmenu i otvara obrazac za uređivanje u pretpreglednom modu. Dozvoljava dodavanje razloga u rezimeu.",
+       "tooltip-undo": "Opcija „Poništi” vraća ovu izmenu i otvara obrazac za uređivanje u režimu pregleda. Dozvoljava dodavanje razloga u rezimeu.",
        "tooltip-preferences-save": "Sačuvaj podešavanja",
        "tooltip-summary": "Unesite kratak opis",
        "interlanguage-link-title": "$1 — $2",
        "pageinfo-display-title": "Naslov za prikaz",
        "pageinfo-default-sort": "Podrazumevani ključ sortiranja",
        "pageinfo-length": "Dužina stranice (u bajtovima)",
+       "pageinfo-namespace": "Imenski prostor",
        "pageinfo-article-id": "ID stranice",
        "pageinfo-language": "Jezik sadržaja stranice",
        "pageinfo-language-change": "promeni",
        "pageinfo-visiting-watchers": "Broj nadgledača stranice koji su posetili skorašnje izmene",
        "pageinfo-few-watchers": "Manje od $1 {{PLURAL:$1|nadgledača}}",
        "pageinfo-few-visiting-watchers": "Moguće je da postoji korisnik koji prati i posećuje nedavne promene",
-       "pageinfo-redirects-name": "Broj preusmeravanja na ovu stranicu",
+       "pageinfo-redirects-name": "Broj preusmerenja na ovu stranicu",
        "pageinfo-redirects-value": "$1",
        "pageinfo-subpages-name": "Broj podstranica ove stranice",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|preusmerenje|preusmerenja|preusmerenja}}; $3 {{PLURAL:$3|nepreusmerenje|nepreusmerenja|nepreusmerenja}})",
        "pageinfo-file-hash": "Hash vrednost",
        "pageinfo-view-protect-log": "Prikaži dnevnik zaštite za ovu stranicu.",
        "markaspatrolleddiff": "Označi kao patrolirano",
-       "markaspatrolledtext": "Označi stranicu kao patroliranu",
-       "markaspatrolledtext-file": "Označi ovu verziju datoteke kao patroliranu",
-       "markedaspatrolled": "Označeno kao patrolirano",
-       "markedaspatrolledtext": "Izabrana izmena stranice [[:$1]] označena je kao patrolirana.",
+       "markaspatrolledtext": "Označi ovu stranicu patroliranom",
+       "markaspatrolledtext-file": "Označi ovu verziju datoteke patroliranom",
+       "markedaspatrolled": "Označeno patroliranim",
+       "markedaspatrolledtext": "Izabrana izmena stranice [[:$1]] označena je patroliranom.",
        "rcpatroldisabled": "Patroliranje skorašnjih izmena je onemogućeno",
        "rcpatroldisabledtext": "Funkcija patroliranja skorašnjih izmena je trenutno onemogućena.",
-       "markedaspatrollederror": "Nije moguće označiti kao patrolirano",
-       "markedaspatrollederrortext": "Morate navesti izmenu da biste je označili kao patroliranu.",
-       "markedaspatrollederror-noautopatrol": "Ne možete da označite svoje promene kao patrolirane.",
-       "markedaspatrollednotify": "Ova promena na stranici „$1” označena je kao patrolirana.",
-       "markedaspatrollederrornotify": "Označavanje ove izmene patroliranom nije uspelo.",
+       "markedaspatrollederror": "Nije moguće označiti patroliranim",
+       "markedaspatrollederrortext": "Morate navesti izmenu da biste je označili patroliranom.",
+       "markedaspatrollederror-noautopatrol": "Nije vam dozvoljeno da označavate sopstvene promene patroliranim.",
+       "markedaspatrollednotify": "Ova promena na stranici „$1” označena je patroliranom.",
+       "markedaspatrollederrornotify": "Označavanje patroliranom nije uspelo.",
        "patrol-log-page": "Dnevnik patroliranja",
        "patrol-log-header": "Ovo je dnevnik patroliranih izmena.",
        "confirm-markpatrolled-button": "U redu",
-       "confirm-markpatrolled-top": "Označiti izmenu $3 stranice $2 kao patroliranu?",
+       "confirm-markpatrolled-top": "Označiti izmenu $3 stranice $2 patroliranom?",
        "deletedrevision": "Izbrisana stara izmena $1.",
        "filedeleteerror-short": "Greška pri brisanju datoteke: $1",
        "filedeleteerror-long": "Došlo je do grešaka pri brisanju datoteke:\n\n$1",
        "previousdiff": "← Starija izmena",
        "nextdiff": "Novija izmena →",
        "mediawarning": "<strong>Upozorenje:</strong> ovaj tip datoteke može da sadrži štetan kod.\nNjegovim izvršavanjem možete da ugrozite vaš sistem.",
-       "imagemaxsize": "Ograničenje veličine slike na stranicama za opis datoteka:",
+       "imagemaxsize": "Ograniči veličinu slike na stranicama za opis datoteka:",
        "thumbsize": "Veličina sličice:",
        "widthheight": "$1 × $2",
        "widthheightpage": "$1 × $2, $3 {{PLURAL:$3|stranica|stranice|stranica}}",
        "metadata-langitem-default": "$1",
        "namespacesall": "svi",
        "monthsall": "sve",
-       "confirmemail": "Potvrda imejl-adrese",
-       "confirmemail_noemail": "Niste postavili važeću imejl-adresu u [[Special:Preferences|korisničkim podešavanjima]].",
+       "confirmemail": "Potvrda adrese e-pošte",
+       "confirmemail_noemail": "Niste postavili važeću adresu e-pošte u [[Special:Preferences|korisničkim podešavanjima]].",
        "confirmemail_text": "{{SITENAME}} zahteva da proverite valjanost imejl-adrese pre nego što počnete da koristite funkciju imejla.\nAktivirajte dugme ispod da biste poslali mejl za potvrdu na svoju adresu.\nMejl će uključivati vezu sa kodom;\nučitajte vezu u svom pregledaču da biste potvrdili da je vaša imejl-adresa važeća.",
        "confirmemail_pending": "Kod za potvrdu vam je već poslat imejlom.\nAko ste nedavno otvorili nalog, možda treba da sačekate nekoliko minuta da pristigne pre nego što ponovo zatražite novi kod.",
        "confirmemail_send": "Pošalji kod za potvrdu",
        "confirmemail_sent": "Potvrdna poruka je poslata.",
        "confirmemail_oncreate": "Kôd za potvrdu je poslat na vašu imejl-adresu.\nOvaj kôd nije neophodan za prijavljivanje, ali ćete morati da ga navedete pre omogućavanja bilo kakvih funkcija zasnovanih na imejlu na vikiju.",
-       "confirmemail_sendfailed": "{{SITENAME}} ne može da pošalje imejl potvrdu.\nProverite da li je imejl adresa pravilno napisana.\n\nGreška: $1",
+       "confirmemail_sendfailed": "{{SITENAME}} ne može da pošalje e-poruku za potvrdu.\nProverite da li je adresa e-pošte sadrži nevažeće znake.\n\nPošiljalac je vratio grešku: $1",
        "confirmemail_invalid": "Nevažeći kod za potvrdu.\nKod je možda istekao.",
-       "confirmemail_needlogin": "$1 da biste potvrdili svoju imejl-adresu.",
-       "confirmemail_success": "Vaša imejl-adresa je potvrđena.\nSada možete da se [[Special:UserLogin|prijavite]] i uživate u vikiju.",
-       "confirmemail_loggedin": "Vaša imejl-adresa je sada potvrđena.",
-       "confirmemail_subject": "{{SITENAME}} – potvrda imejl-adrese",
+       "confirmemail_needlogin": "$1 da biste potvrdili adresu e-pošte.",
+       "confirmemail_success": "Vaša adresa e-pošte je potvrđena.\nSada možete da se [[Special:UserLogin|prijavite]] i uživate u vikiju.",
+       "confirmemail_loggedin": "Vaša adresa e-pošte je sada potvrđena.",
+       "confirmemail_subject": "{{SITENAME}} – potvrda adrese e-pošte",
        "confirmemail_body": "Neko, verovatno Vi, sa IP adrese $1,\nregistrovao je nalog „$2“ sa ovom imejl adresom na projektu {{SITENAME}}.\n\nDa biste potvrdili da ovaj nalog stvarno pripada vama i aktivirali funkciju imejla na projektu {{SITENAME}}, otvorite ova u pregledaču:\n\n$3\n\nAko vi *niste* registrovali nalog, pratite ovu vezu\nda biste otkazali potvrdu imejl adrese:\n\n$5\n\nOvaj kod za potvrdu ističe u $4.",
        "confirmemail_body_changed": "Neko, verovatno Vi, s IP adrese $1,\npromenio je imejl adresu naloga „$2“ u ovu adresu na projektu {{SITENAME}}.\n\nDa biste potvrdili da ovaj nalog stvarno pripada vama i ponovo aktivirali funkciju imejla, otvorite sledeću vezu u pregledaču:\n\n$3\n\nAko nalog *ne* pripada vama, pratite sledeću vezu da otkažete potvrdu imejl adrese:\n\n$5\n\nOvaj kod za potvrdu ističe $6 u $7",
        "confirmemail_body_set": "Neko, verovatno Vi, s IP adrese $1,\npromenio je imejl adresu naloga „$2“ u ovu adresu na {{SITENAME}}.\n\nDa bismo potvrdili da ovaj nalog stvarno pripada vama i ponovo aktivirali\nfunkciju imejla na {{SITENAME}}, otvorite sledeću vezu u pregledaču:\n\n$3\n\nAko nalog *ne* pripada vama, pratite sledeću vezu da otkažete potvrdu imejl adrese:\n\n$5\n\nOvaj kod za potvrdu ističe $4.",
-       "confirmemail_invalidated": "Potvrda imejl adrese je otkazana",
-       "invalidateemail": "Otkazivanje potvrde imejla",
+       "confirmemail_invalidated": "Potvrda adrese e-pošte je otkazana",
+       "invalidateemail": "Otkazivanje potvrde e-pošte",
        "notificationemail_subject_changed": "Registrovana imejl adresa na projektu {{SITENAME}} je promenjena",
        "notificationemail_subject_removed": "Registrovana imejl adresa na projektu {{SITENAME}} je uklonjena",
        "notificationemail_body_changed": "Neko, verovatno Vi je promenio imejl adresu naloga iz $2“ u „$3“ sa IP adrese $1 na sajtu {{SITENAME}}.\n\nAko ovo niste bili Vi, odmah obavestite administratore sajta.",
        "confirm-purge-top": "Obrisati keš ove stranice?",
        "confirm-purge-bottom": "Osvežavanje stranice briše keš i nameće najnoviju izmenu.",
        "confirm-watch-button": "U redu",
-       "confirm-watch-top": "Dodati ovu stranicu u spisak nadgledanja?",
+       "confirm-watch-top": "Dodati ovu stranicu na spisak nadgledanja?",
        "confirm-unwatch-button": "U redu",
        "confirm-unwatch-top": "Ukloniti ovu stranicu sa spiska nadgledanja?",
        "confirm-rollback-button": "U redu",
        "autosumm-blank": "Uklonjen celokupan sadržaj stranice",
        "autosumm-replace": "Zamenjen sadržaj stranice sa „$1“",
        "autoredircomment": "Preusmerena stranica na [[$1]]",
-       "autosumm-removed-redirect": "Uklonjeno preusmeravanje na [[$1]]",
+       "autosumm-removed-redirect": "Uklonjeno preusmerenje na [[$1]]",
        "autosumm-changed-redirect-target": "Promenjena odredišna stranica u preusmerenju iz [[$1]] u [[$2]]",
        "autosumm-new": "Nova stranica: $1",
        "autosumm-newblank": "Napravljena prazna stranica",
        "version-license-not-found": "Za ovaj dodatak nije pronađena informacija o licenci.",
        "version-credits-title": "Zasluge za $1",
        "version-credits-not-found": "Za ovaj dodatak nije pronađena informacija o zaslugama.",
-       "version-poweredby-credits": "Ovaj viki pokreće '''[https://www.mediawiki.org/ Medijaviki]''', autorska prava © 2001-$1 $2.",
+       "version-poweredby-credits": "Ovaj viki pokreće <strong>[https://www.mediawiki.org/ MediaWiki]</strong>, autorska prava © 2001-$1 $2.",
        "version-poweredby-others": "ostali",
        "version-poweredby-translators": "translatewiki.net prevodioci",
        "version-credits-summary": "Želeli bismo da zahvalimo sledećim ljudima na njihovom doprinosu [[Special:Version|Medijavikiji]].",
        "version-libraries-license": "Licenca",
        "version-libraries-description": "Opis",
        "version-libraries-authors": "Autori",
-       "redirect": "Preusmeravanje na datoteku, korisnika, stranicu, izmenu ili dnevnik (ID)",
+       "redirect": "Preusmerenje na datoteku, korisnika, stranicu, izmenu ili dnevnik (ID)",
        "redirect-summary": "Ova posebna stranica preusmerava do datoteke (s datim imenom datoteke), stranice (s datim ID-om izmene ili ID-om stranice), korisničke stranice (s datim numeričkim korisničkim ID-om), ili unosa u dnevniku (s datim dnevničkim ID-om). Upotreba: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/page/64308]], [[{{#Special:Redirect}}/revision/328429]], [[{{#Special:Redirect}}/user/101]], or [[{{#Special:Redirect}}/logid/186]].",
        "redirect-submit": "Idi",
        "redirect-lookup": "Tip vrednosti:",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|oznaka|oznake}}]]: $2",
        "tag-mw-contentmodelchange": "promena modela sadržaja",
        "tag-mw-contentmodelchange-description": "Izmene koje [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel menjaju model sadržaja] stranice",
-       "tag-mw-new-redirect": "novo preusmeravanje",
-       "tag-mw-new-redirect-description": "Izmene kojima je napravljeno novo preusmeravanje ili je stranica promenjena u preusmeravanje",
-       "tag-mw-removed-redirect": "uklonjeno preusmeravanje",
-       "tag-mw-removed-redirect-description": "Izmene koje menjaju postojeće preusmeravanje u nepreusmeravanje",
+       "tag-mw-new-redirect": "novo preusmerenje",
+       "tag-mw-new-redirect-description": "Izmene kojima je napravljeno novo preusmerenje ili je stranica promenjena u preusmerenje",
+       "tag-mw-removed-redirect": "uklonjeno preusmerenje",
+       "tag-mw-removed-redirect-description": "Izmene koje menjaju postojeće preusmerenje u nepreusmerenje",
        "tag-mw-changed-redirect-target": "promenjeno odredište preusmerenja",
        "tag-mw-changed-redirect-target-description": "Izmene koje menjaju odredište preusmerenja",
        "tag-mw-blank": "stranica ispražnjena",
        "htmlform-user-not-exists": "<strong>$1</strong> ne postoji.",
        "htmlform-user-not-valid": "<strong>$1</strong> nije validno korisničko ime.",
        "logentry-delete-delete": "$1 je {{GENDER:$2|izbrisao|izbrisala}} stranicu $3",
-       "logentry-delete-delete_redir": "$1 je {{GENDER:$2|izbrisao|izbrisala}} preusmeravanje $3 prepisivanjem",
+       "logentry-delete-delete_redir": "$1 je {{GENDER:$2|izbrisao|izbrisala}} preusmerenje $3 prepisivanjem",
        "logentry-delete-restore": "$1 je {{GENDER:$2|vratio|vratila}} stranicu $3 ($4)",
        "logentry-delete-restore-nocount": "$1 je {{GENDER:$2|vratio|vratila}} stranicu $3",
        "restore-count-revisions": "{{PLURAL:$1|1 izmena|$1 izmene|$1 izmena}}",
        "logentry-move-move-noredirect": "$1 je {{GENDER:$2|premestio|premestila}} stranicu $3 na $4 bez ostavljanja preusmerenja",
        "logentry-move-move_redir": "$1 je {{GENDER:$2|premestio|premestila}} stranicu $3 na $4 preko preusmerenja",
        "logentry-move-move_redir-noredirect": "$1 je {{GENDER:$2|premestio|premestila}} stranicu $3 na $4 preko preusmerenja bez ostavljanja preusmerenja",
-       "logentry-patrol-patrol": "$1 je {{GENDER:$2|označio|označila}} izmenu $4 stranice $3 kao patroliranu",
-       "logentry-patrol-patrol-auto": "$1 je automatski {{GENDER:$2|označio|označila}} izmenu $4 stranice $3 kao patroliranu",
+       "logentry-patrol-patrol": "$1 je {{GENDER:$2|označio|označila}} izmenu $4 stranice $3 patroliranom",
+       "logentry-patrol-patrol-auto": "$1 je automatski {{GENDER:$2|označio|označila}} izmenu $4 stranice $3 patroliranom",
        "logentry-newusers-newusers": "$1 je {{GENDER:$2|otvorio|otvorila}} korisnički nalog",
        "logentry-newusers-create": "$1 je {{GENDER:$2|otvorio|otvorila}} korisnički nalog",
        "logentry-newusers-create2": "$1 je {{GENDER:$2|otvorio|otvorila}} korisnički nalog $3",
        "log-action-filter-newusers-create": "otvorio anoniman korisnik",
        "log-action-filter-newusers-create2": "otvorio registrovan korisnik",
        "log-action-filter-newusers-autocreate": "automatski otvoren",
-       "log-action-filter-newusers-byemail": "otvaranje lozinkom poslanom na imejlu",
+       "log-action-filter-newusers-byemail": "otvaranje lozinkom poslanom e-porukom",
        "log-action-filter-patrol-patrol": "ručno",
        "log-action-filter-patrol-autopatrol": "automatsko",
        "log-action-filter-protect-protect": "zaključavanje",
        "authmanager-password-help": "Lozinka za potvrdu identiteta.",
        "authmanager-domain-help": "Domen za spoljašnju potvrdu identiteta.",
        "authmanager-retype-help": "Ponovite lozinku da bi ste potvrdili.",
-       "authmanager-email-label": "Imejl",
-       "authmanager-email-help": "Imejl-adresa",
+       "authmanager-email-label": "E-pošta",
+       "authmanager-email-help": "Adresa e-pošte:",
        "authmanager-realname-label": "Pravo ime",
        "authmanager-realname-help": "Pravo ime korisnika",
        "authmanager-provider-password": "Potvrda identiteta lozinkom",
index d96d053..0b8133b 100644 (file)
@@ -24,7 +24,7 @@
        "tog-usenewrc": "ಇಂಚಿಪೊದ ಬದಲಾವಣೆ ಬೊಕ್ಕೊ ವೀಕ್ಷಣಾಪಟ್ಟಿಡ್ ಪುಟೊತ ಅನುಸಾರ ಗುಂಪು ಬದಲಾವಣೆಲು",
        "tog-numberheadings": "ತರೆಬರವುಲೆಗ್ ಕ್ರಮಸಂಖ್ಯೆಲೆನ್ ತೋಜಾವು",
        "tog-editondblclick": "ರಡ್ಡ್ ಸರ್ತಿ ಒತ್ತ್‌ನಗ ಪುಟೊನು ಸಂಪೊಲಿಪುನಂಚ ಆವಡ್",
-       "tog-editsectiononrightclick": "ಪà³\81à²\9fà³\8aತ à²µà²¿à²¬à²¾à²\97à³\8aಲà³\86ನà³\8d à²\90ತ à²¸à³\80ರà³\8dಸಿà²\95à³\86ನà³\8d à²°à²¡à³\8dಡà³\8d à²¸à²°à³\8dತಿ à²\92ತà³\8dತà³\8dâ\80\8cನà²\97 ಸಂಪೊಲಿಪುನಂಚ ಉಪ್ಪಡ್",
+       "tog-editsectiononrightclick": "ವಿಬಾà²\97à³\8a  à²¸à³\80ರà³\8dಸಿà²\95à³\86ಲà³\86ನ à²¬à²²à²\97à³\81ಬà³\8dಬಿ à²\92ತà³\8dತà³\8dâ\80\8cನà²\97 à²µà²¿à²­à²¾à²\97à³\8a ಸಂಪೊಲಿಪುನಂಚ ಉಪ್ಪಡ್",
        "tog-watchcreations": "ಯಾನ್ ಉಂಡುಮಲ್ತಿನ ಪುಟೊಕ್ಲೆನ್ ಬೊಕ್ಕ ಅಪ್ಲೋಡ್ ಮಲ್ತಿ ಕಡತೊಲೆನ್ ಎನ್ನ ವೀಕ್ಷಣಾಪಟ್ಟಿಗ್ ಸೇರ್ಪಾಲೆ",
        "tog-watchdefault": "ಯಾನ್ ಸಂಪೊಲಿಪುನ ಪುಟೊಕ್ಲೆನ್ ಬೊಕ್ಕ ಕಡತೊಲೆನ್ ವೀಕ್ಷಣಾಪಟ್ಟಿಗ್ ಸೇರ್ಪಾಲೆ",
        "tog-watchmoves": "ಯಾನ್ ಸ್ತಲಾಂತರಿಪುನ ಪುಟೊಕ್ಲೆನ್ ಬೊಕ್ಕ ಕಡತೊಲೆನ್ ಎನ್ನ ವೀಕ್ಷಣಾಪಟ್ಟಿಗ್ ಸೇರ್ಪಾಲೆ",
        "tog-shownumberswatching": "ಪುಟೊನು ತೂವೊಂದುಪ್ಪುನಂಚಿನ ಸದಸ್ಯೆರ್‍ನ ಸಂಖ್ಯೆನ್ ತೊಜ್ಪಾಲೆ",
        "tog-oldsig": "ಇತ್ತೆ ಉಪ್ಪುನ ದಸ್ಕತ್ತ್",
        "tog-fancysig": "ದಸ್ಕತ್ತ್‌ನ್ ವಿಕಿಟೆಕ್ಷ್ಟ್ ಆದ್ ದೆತ್ತೊನು (ಸ್ವಯಂ ಕೊಂಡಿ ದಾಂತೆ)",
-       "tog-uselivepreview": "ನà³\87ರà³\8a à²®à³\81ನà³\8dನà³\8bà²\9fà³\8aನà³\81 à²\89ಪಯà³\8bà²\97 à²®à²²à³\8dಪà³\81ಲೆ",
-       "tog-forceeditsummary": "ಸà²\82ಪಾದನà³\86 à²¸à²¾à²°à²¾à²\82ಸà³\8aನà³\81 à²\95ಾಲಿ à²¬à³\81ಡà³\8dâ\80\8dà²\82ಡ à²\8eà²\82à²\95à³\8d à²¨à³\86ನಪà³\81 à²®à²²à³\8dಪà³\81ಲೆ",
+       "tog-uselivepreview": "ಪà³\81à²\9fà³\8aನà³\81 à²\95à³\81ಡಾ-à²\95ಣವà²\82ದà³\86 à²®à³\81ನà³\8dನà³\8bà²\9fà³\8aನà³\81 à²¤à³\8bà²\9cಾಲೆ",
+       "tog-forceeditsummary": "à²\95ಾಲಿ à²¸à²\82ಪಾದನà³\86 à²¸à²¾à²°à²¾à²\82ಸà³\8aನà³\81  à²¸à³\87ರಾನà²\97 à²\8eà²\82à²\95à³\8d à²¤à³\86ರಿಪಾಲೆ",
        "tog-watchlisthideown": "ವೀಕ್ಷಣಾಪಟ್ಟಿಡ್ ಎನ್ನ ಸಂಪಾದನೆಲೆನ್ ದೆಂಗಾಲೆ",
        "tog-watchlisthidebots": "ವೀಕ್ಷಣಾಪಟ್ಟಿಡ್ ಬಾಟ್ ಸಂಪಾದನೆಲೆನ್ ದೆಂಗಾಲೆ",
        "tog-watchlisthideminor": "ಎಲ್ಯ ಬದಲಾವಣೆಲೆನ್ ವೀಕ್ಷಣಾಪಟ್ಟಿರ್ದ್ ದೆಂಗಾಲೆ",
        "tog-watchlisthideliu": "ಲಾಗಿನ್ ಆತಿನಂಚಿನ ಸದಸ್ಯೆರ್‍ನ ಸಂಪಾದನೆಲೆನ್ ವೀಕ್ಷಣಾಪಟ್ಟಿರ್ದ್ ದೆಂಗಾಲೆ",
        "tog-watchlistreloadautomatically": "ಅರಿಪೆ ಬದಲಾನಗ ವೀಕ್ಷಣಾಪಟ್ಟಿ ಕುಡೊರ ಲೋಡ್ ಆವಡ್ (ಜಾವಾಸ್ಕ್ರಿಪ್ಟ್ ಉಪ್ಪೊಡು)",
+       "tog-watchlistunwatchlinks": "ಬದಲಾವಣೆ ಇತ್ತಿನ ವೀಕ್ಷಿತ ಪುಟೊಲೆಗ್ ಅವೀಕ್ಷಕ/ವೀಕ್ಷಕ ಗುರ್ತಿಕೆಲೆನ್ ಸೀದಾ ಸೇರಾಲೆ ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) (ಪುರೆಲ್'ಗುಬ್ಬಿ ಕಾರ್ಯಕರಣೊಗು ಜಾವಾಸ್ಕ್ರಿಪ್ಟ್ JavaScript ಬೋಡಾಪುಂಡು )",
        "tog-watchlisthideanons": "ಪುದರಿದಾಂತಿ ಗಲಸುನಾರೆನ ಸಂಪಾದನೆಲೆನ್ ವೀಕ್ಷಣಾಪಟ್ಟಿಡ್ದ್ ದೆಂಗಾಲೆ",
        "tog-watchlisthidepatrolled": "ಪರೀಕ್ಷಣೆ ಮಲ್ತಿನ ಸಂಪಾದನೆಲೆನ್ ವೀಕ್ಷಣಾಪಟ್ಟಿಡ್ದ್ ದೆಂಗಾಲೆ",
        "tog-watchlisthidecategorization": "ಪುಟೊಕ್ಲೆನ ವರ್ಗೀಕರಣೊನು ದೆಂಗಾಲೆ",
@@ -60,7 +61,7 @@
        "underline-always": "ಯಾಪಲ",
        "underline-never": "ಯಾಪಗ್ಲಾ ಇಜ್ಜಿ",
        "underline-default": "ಬ್ರೌಸರ್‍ದ ಯತಾಸ್ತಿತಿ",
-       "editfont-style": "ಬರà³\86ಪà³\81ನ à²\9cಾà²\97ದ à²\85à²\95à³\8dಷರದ ಶೈಲಿ:",
+       "editfont-style": "ಸà²\82ಪಾದಿ à²\9cಾà²\97ದ à²\85à²\95à³\8dಷರ ಶೈಲಿ:",
        "editfont-monospace": "ಒಂಜಿ ಜಾಗೆದ ಮುದ್ರೆಲಿಪಿ",
        "editfont-sansserif": "ಸಾನ್ಸ್-ಸೆರಿಫ್ ಲಿಪಿ",
        "editfont-serif": "ಸೆರಿಫ್ ಲಿಪಿ",
        "pagecategories": "{{PLURAL:$1|ವರ್ಗೊ|ವರ್ಗೊಲು}}",
        "category_header": "\"$1\" ವರ್ಗಡುಪ್ಪುನಂಚಿನ ಲೇಕನೊಲು",
        "subcategories": "ಉಪವರ್ಗೊಲು",
-       "category-media-header": "\"$1\" ವರ್ಗಡುಪ್ಪುನಂಚಿನ ಚಿತ್ರೊ/ಶಬ್ಧೊ ಫೈಲ್‍ಲು",
+       "category-media-header": "\"$1\" ವರ್ಗಡುಪ್ಪುನ ಮಾಧ್ಯಮೊಲು",
        "category-empty": "''ಈ ವರ್ಗೊಡು ಸದ್ಯಗ್ ಓವುಲ ಪುಟೊಕುಲಾವಡ್ ಅತ್ತಂಡ ಚಿತ್ರೊಲಾವಡ್ ಇಜ್ಜಿ.''",
        "hidden-categories": "{{PLURAL:$1|ದೆಂಗಾದ್ ದೀತಿನ ವರ್ಗೊ|ದೆಂಗಾದ್ ದೀತಿನ ವರ್ಗೊಲು}}",
        "hidden-category-category": "ದೆಂಗಾದ್ ದೀತಿನ ವರ್ಗೊಲು",
        "category-article-count": "{{PLURAL:$2|ಈ ವರ್ಗೊಡು ತಿರ್ತ್ ಉಪ್ಪುನ ಒಂಜಿ ಪುಟೊ ಮಾತ್ರ ಉಂಡು|ಒಟ್ಟು $2 ಪುಟೊಕುಲೆಡ್ ತಿರ್ತ್ ಉಪ್ಪುನ {{PLURAL:$1|ಪುಟೊ|$1 ಪುಟೊಕುಲು}} ಈ ವರ್ಗೊಡು ಉಂಡು.}}",
        "category-article-count-limited": "ತಿರ್ತ್ ಕೊರ್ತಿನ {{PLURAL:$1|ಪುಟ|$1 ಪುಟೊಕುಲು}} ಈ ವರ್ಗೊಡು ಉಂಡು.",
        "category-file-count": "{{PLURAL:$2|ತಿರ್ತ್ ಕೊರ್ತಿನ ಒಂಜಿ ಫೈಲ್ ಮಾತ್ರ ಈ ವರ್ಗೊಡು ಉಂಡು.|ಒಟ್ಟು $2 ಫೈಲ್‌ಲೆಡ್, ತಿರ್ತ್ ಕೊರ್ತಿನ {{PLURAL:$1|ಫೈಲ್‍|$1 ಫೈಲ್‍ಲು}} ಈ ವರ್ಗೊಡು ಉಂಡು.}}",
-       "category-file-count-limited": "ತಿರ್ತ್ ಕೊರ್ತಿನ {{PLURAL:$1|ಫೈಲ್|$1 ಫೈಲ್‌ಲು}} ಈ ವರ್ಗೊಡು ಉಂಡು.",
+       "category-file-count-limited": "ತಿರ್ತ್ ಕೊರ್ತಿನ {{PLURAL:$1|ಫೈಲ್..|$1 ಫೈಲ್‌ಲು..}} ಚಾಲ್ತಿ ವರ್ಗೊಡು ಉಂಡು.",
        "listingcontinuesabbrev": "ದುಂಬು.",
-       "index-category": "ಸà³\82à²\9aಿà²\95à³\8dರಮà³\8aà²\9fಿತà³\8dತಿ ಪುಟಕುಲು",
-       "noindex-category": "ಸà³\82à²\9aಿà²\95à³\8dರಮà³\8aà²\9fಿà²\9cà³\8dà²\9cಾà²\82ದಿನ ಪುಟೊಕುಲು",
-       "broken-file-category": "ಕಡಿದಿನ ಕಡತ ಕೊಂಡಿಲು ಉಪ್ಪುನ ಪುಟೊಕುಲು",
-       "about": "à²\8eà²\82à²\95à³\8dಲà³\86ನ à²¬à²\97à³\8dà²\97à³\86",
-       "article": "ಲà³\87à²\96ನ ಪುಟ",
+       "index-category": "ಸà³\82à²\9aಿತ ಪುಟಕುಲು",
+       "noindex-category": "à²\85ಸà³\82à²\9aಿತ ಪುಟೊಕುಲು",
+       "broken-file-category": "ಕಡಿದಿನ ಕಡತ ಕೊಂಡಿಲು ಉಪ್ಪುನ ಪುಟೊಕುಲು",
+       "about": "ಬಗ್ಗೆ",
+       "article": "ಪರಿವಿಡಿ ಪುಟ",
        "newwindow": "(ಪೊಸ ಕಂಡಿನ್ ದೆಪ್ಪುಂಡು)",
        "cancel": "ವಜಾ ಮಲ್ಪುಲೆ",
-       "moredotdotdot": "ನನಲ...",
-       "morenotlisted": "à²\88 à²ªà²\9fà³\8dà²\9fಿ à²ªà³\82ರà³\8dತಿ à²\86ತà³\8dâ\80\8dà²\9cಿ.",
-       "mypage": "à²\8eನà³\8dನ à²ªà³\81à²\9fà³\8a",
-       "mytalk": "à²\8eನà³\8dನ à²\9aರà³\8dà²\9aà³\86",
-       "anontalk": "ಪಾತೆರ್ಲೆ",
+       "moredotdotdot": "ನನಾತà³\8d...",
+       "morenotlisted": "à²\88 à²ªà²\9fà³\8dà²\9fಿ à²\85ಪà³\82ರà³\8dಣ à²\86ದಿಪà³\8dಪà³\81.",
+       "mypage": "ಪುಟೊ",
+       "mytalk": "ಪಾತà³\86ರ",
+       "anontalk": "ಪಾತೆರ",
        "navigation": "ಸಂಚಾರೊ",
        "and": "&#32;ಬೊಕ್ಕ",
        "faq": "ಸಾಮಾನ್ಯವಾದ್ ಕೇನುನ ಪ್ರಶ್ನೆಲು",
        "returnto": "$1ಗ್ ಪಿರಪೋಲೆ.",
        "tagline": "{{SITENAME}}ರ್ದ್",
        "help": "ಸಹಾಯೊ",
+       "help-mediawiki": "ಮಿಡಿಯಾವಿಕಿತ ಬಗ್ಗೆ ಸಹಾಯ",
        "search": "ನಾಡ್‍ಲೆ",
-       "search-ignored-headings": "#<!--à²\88 à²²à³\88ನà³\8dâ\80\8cನà³\8d à²\8eà²\82à²\9a à²\89à²\82ಡà³\81 à²\85à²\82à²\9aà³\86ನà³\86 à²¬à³\81ಡà³\8dಲà³\86--> <pre>\n# à²¨à²¾à²¡à³\8dâ\80\8cನà²\97 à²\85ಲà²\95à³\8dಷà³\8dಯ à²®à²²à³\8dಪà³\8aಡಾಯಿನ à²¤à²°à³\86ಬರವà³\81ಲà³\81.\n# à²¤à²°à³\86ಬರವà³\81 à²\87ತà³\8dತಿ à²ªà³\81à²\9fà³\8a à²\87à²\82ಡà³\86à²\95à³\8dಸà³\8d à²\86ನà²\97ನà³\87, à²¨à³\86à²\95à³\8dà²\95à³\8d à²\86ಪಿನ à²¬à²¦à²²à²¾à²µà²£à³\86ಲà³\81 à²¤à³\8bà²\9cà³\81à²\82ಡà³\81.\n# à²\88ರà³\8d à²¶à³\82ನà³\8dಯ à²¸à²\82ಪಾದನà³\86 à²®à²²à³\8dತà³\8dâ\80\8cದà³\8d à²\92à²\82à²\9cಿ à²ªà³\81à²\9fà³\8aನà³\81 à²\95à³\81ಡ à²\87à²\82ಡà³\86à²\95à³\8dಸà³\8d à²\86ಪಿಲà³\86à²\95à³\8a à²®à²²à³\8dಪà³\8aಲಿ. \n# à²µà²¾à²\95à³\8dಯರà²\9aಣà³\86 à²\87à²\82à²\9a à²\89à²\82ಡà³\81:\n#   * \"#\" à²\85à²\95à³\8dಷರà³\8aಡà³\8dದà³\81 à²²à³\88ನà³\8dâ\80\8cದ à²\95ಡà³\86 à²®à³\81à²\9fà³\8dà²\9f à²\89ಪà³\8dಪà³\81ನ à²®à²¾à²¤à²¾ à²\9fಿಪà³\8dಪಣಿ.\n#   * à²\96ಾಲಿ à²\85ತà³\8dತಾà²\82ದಿನ à²\92à²\82à²\9cà³\8aà²\82à²\9cಿ à²²à³\88ನà³\8dâ\80\8cಲಾ à²\85à²\95à³\8dಷರ à²¨à²®à³\82ನà³\86 à²¬à³\8aà²\95à³\8dà²\95 à²®à²¾à²¤à³\86ನà³\8dಲಾ à²\85ಲà²\95à³\8dಷà³\8dಯ à²®à²²à³\8dಪà³\81ನ à²¤à²°à³\86ಬರವà³\81.\nà²\89ಲà³\8dಲà³\87à²\95à³\8a\nಪಿದಯಿದ à²\95à³\8aà²\82ಡಿಲà³\81\nà²\89à²\82ದà³\86ನà³\8dಲಾ à²¤à³\82ಲà³\86\n #</pre> <!--à²\88 à²²à³\88ನà³\8dâ\80\8cನ್ ಎಂಚ ಉಂಡು ಅಂಚೆನೆ ಬುಡ್ಲೆ-->",
+       "search-ignored-headings": "#<!--à²\88 à²²à³\88ನà³\8dâ\80\8cನà³\8d à²\8eà²\82à²\9a à²\89à²\82ಡà³\81 à²\85à²\82à²\9aà³\86ನà³\86 à²¬à³\81ಡà³\8dಲà³\86--> <pre>\n# à²¨à²¾à²¡à³\8dâ\80\8cನà²\97 à²\85ಲà²\95à³\8dಷà³\8dಯ à²®à²²à³\8dಪà³\8aಡಾಯಿನ à²¤à²°à³\86ಬರವà³\81ಲà³\81.\n# à²¤à²°à³\86ಬರವà³\81 à²\87ತà³\8dತಿ à²ªà³\81à²\9fà³\8a à²\87à²\82ಡà³\86à²\95à³\8dಸà³\8d à²\86ನà²\97ನà³\87, à²¨à³\86à²\95à³\8dà²\95à³\8d à²\86ಪಿನ à²¬à²¦à²²à²¾à²µà²£à³\86ಲà³\81 à²¤à³\8bà²\9cà³\81à²\82ಡà³\81.\n# à²\88ರà³\8d à²¶à³\82ನà³\8dಯ à²¸à²\82ಪಾದನà³\86 à²®à²²à³\8dತà³\8dâ\80\8cದà³\8d à²\92à²\82à²\9cಿ à²ªà³\81à²\9fà³\8aನà³\81 à²\95à³\81ಡ à²\87à²\82ಡà³\86à²\95à³\8dಸà³\8d à²\86ಪಿಲà³\86à²\95à³\8a à²®à²²à³\8dಪà³\8aಲಿ. \n# à²µà²¾à²\95à³\8dಯರà²\9aಣà³\86 à²\87à²\82à²\9a à²\89à²\82ಡà³\81:\n#   * \"#\" à²\85à²\95à³\8dಷರà³\8aಡà³\8dದà³\81 à²\97à³\86ರà³\86ತ à²\95ಡà³\86 à²®à³\81à²\9fà³\8dà²\9f à²\89ಪà³\8dಪà³\81ನ à²®à²¾à²¤à²¾ à²\9fಿಪà³\8dಪಣಿ.\n#   * à²\96ಾಲಿ à²\85ತà³\8dತಾà²\82ದಿನ à²\92à²\82à²\9cà³\8aà²\82à²\9cಿ à²\97à³\86ರà³\86ಲಾ à²\85à²\95à³\8dಷರ à²¨à²®à³\82ನà³\86 à²¬à³\8aà²\95à³\8dà²\95 à²®à²¾à²¤à³\86ನà³\8dಲಾ à²\85ಲà²\95à³\8dಷà³\8dಯ à²®à²²à³\8dಪà³\81ನ à²¤à²°à³\86ಬರವà³\81.\nà²\89ಲà³\8dಲà³\87à²\95à³\8a\nಪಿದಯಿದ à²\95à³\8aà²\82ಡಿಲà³\81\nà²\89à²\82ದà³\86ನà³\8dಲಾ à²¤à³\82ಲà³\86\n #</pre> <!--à²\88 à²\97à³\86ರà³\86ನ್ ಎಂಚ ಉಂಡು ಅಂಚೆನೆ ಬುಡ್ಲೆ-->",
        "searchbutton": "ನಾಡ್‍ಲೆ",
        "go": "ಪೋ",
        "searcharticle": "ಪೋಲೆ",
        "create-local": "ಸ್ಥಳೀಯ ವಿವರಣೆನ್ ಸೇರಾಲೆ",
        "delete": "ಮಾಜಾಲೆ",
        "undelete_short": "ಮಾಜಾದಿನ {{PLURAL:$1|ಒ೦ಜಿ ಬದಲಾವಣೆನ್|$1 ಬದಲಾವಣೆಲೆನ್}} ಪಿರ ಪಾಡ್ಲೆ",
-       "viewdeleted_short": "{{PLURAL:$1|1 à²¡à²¿à²²à³\80à²\9fà³\8d à²\86ತಿನ à²\92à²\82à²\9cಿ à²¸à²\82ಪಾದನà³\86ನà³\8d|$1 à²¡à²¿à²²à³\80à²\9fà³\8d à²\86ತಿನ ಸಂಪಾದನೆಲೆನ್}} ತೂಲೆ",
+       "viewdeleted_short": "{{PLURAL:$1|1 à²®à²¾à²\9cಾದಿನ à²\92à²\82à²\9cಿ à²¸à²\82ಪಾದನà³\86|$1 à²®à²¾à²\9cಾದಿನ ಸಂಪಾದನೆಲೆನ್}} ತೂಲೆ",
        "protect": "ಸ೦ರಕ್ಷಿಪುಲೆ",
        "protect_change": "ಬದಲ್ಪುಲೆ",
        "unprotect": "ರಕ್ಷಣೆನ್ ಬದಲ್‍ಪುಲೆ",
        "helppage-top-gethelp": "ಸಹಾಯೊ",
        "mainpage": "ಮುಖ್ಯ ಪುಟ",
        "mainpage-description": "ಮುಖ್ಯ ಪುಟ",
-       "policy-url": "Project:ನಿಯಮಾವಳಿ",
+       "policy-url": "Project:à²\95ಾರà³\8dಯನà³\80ತಿ",
        "portal": "ಸಮುದಾಯೊ ಪುಟೊ",
        "portal-url": "Project:ಸಮುದಾಯ ಪುಟೊ",
        "privacy": "ಕಾಸಗಿ ಕಾರ್ಯೊನೀತಿ",
        "versionrequiredtext": "ಈ ಪುಟೊನು ಗಲಸರೆ ಮೀಡಿಯವಿಕಿದ $1 ನೇ ಆವೃತ್ತಿ ಬೋಡು.\n[[Special:Version|ಆವೃತ್ತಿ ಪುಟೊನು]] ತೂಲೆ.",
        "ok": "ಸರಿ",
        "retrievedfrom": "\"$1\"ಡ್ದ್ ದೆತ್ತೊಂದುಂಡು",
-       "youhavenewmessages": "ಇರೆಗ್ $1 ಉಂಡು ($2).",
+       "youhavenewmessages": "{{PLURAL:$3|ಇರೆಗ್ ಉಂಡು}} $1  ($2).",
        "youhavenewmessagesfromusers": "{{PLURAL:$4|ಈರೆಗ್}} {{PLURAL:$3|ನನೊರಿ ಸದಸ್ಯೆಡ್ದ್|$3 ಸದಸ್ಯೆರೆಡ್ದ್}} $1 ಉಂಡು. ($2)",
        "youhavenewmessagesmanyusers": " ನಿಕ್ಲೆಗ್ ದಿಂಜ ಸದಸ್ಯೆರೆಡ್ದ್ $1 ಉಂಡು ($2).",
        "newmessageslinkplural": "{{PLURAL:$1|ಒಂಜಿ ಪೊಸ ಸಂದೇಸೊ|999=ಪೊಸ ಸಂದೇಸೊಲು}}",
        "laggedslavemode": "<strong>ಎಚ್ಚರೊ:</strong> ಪುಟೊಡು ಇಂಚಿಪದ ಬದಲಾವಣೆಲು ಉಪ್ಪಂದ್.",
        "readonly": "ಡಾಟಾಬೇಸ್ ಲಾಕ್ ಆತ್೦ಡ್",
        "enterlockreason": "ಡೇಟಬೇಸ್‌ಗ್ ಲಾಕ್ ಪಾಡುನ ಕಾರಣೊನು ಬೊಕ್ಕೊ ಲಾಕ್‌ನ್ ದೆಪ್ಪುನ ಅಂದಾಜಿದ ಪೊರ್ತುನು ತೆರಿಪಾಲೆ",
+       "readonlytext": "ಪೊಸ ಸೇರಿಕೆ ಬೊಕ ಇತರೆ ಮಾರ್ಪಾಟುಲು ಆವಂದಿಲೆಕ್ಕೊ ದತ್ತಸಂಚಯ ಇತ್ತೆ ಬೀಗ ಪಾಡ್'ದುಂಡು,ಬಹುಷಃ ದತ್ತಸಂಚಯದ ನಿಯತದ ನಿರ್ವಹಣೆಗಾದ್,ಅಯಿಡ್ದ್ ಬೊಕ ಅವು ಸಾಜ ಸ್ಥಿತಿಕ್ ಬರ್ಪುಂಡು. \nದತ್ತಸಂಚಯೊಗು ಬೀಗಪಾಡಿನ ಗಣವ್ಯೂಹ ನಿರ್ವಾಹಕೆರ್ ಕೊರ್ತಿನ ವಿವರಣೆ ಮೂಲುಂಡು:$1",
        "missing-article": "\"$1\" $2 ಪುದರ್’ದ ಪುಟ ದೇಟಬೇಸ್’ಡ್ ಇಜ್ಜಿ.\n\nಡಿಲೀಟ್ ಮಲ್ತಿನ ಪುಟೊಕು ಸಂಪರ್ಕ ಕೊರ್ಪುನ ಇತಿಹಾಸ ಲಿಂಕ್ ಅತ್ತ್’ನ್ಡ ವ್ಯತ್ಯಾಸ ಲಿಂಕ್’ನ್ ಒತ್ತುನೆರ್ದಾದ್ ಈ ದೋಷ ಸಾಧಾರಣವಾದ್ ಬರ್ಪುಂಡು.\n\nಒಂಜಿ ವೇಳೆ ಅಂಚ ಆದಿಜ್ಜಿಂಡ, ಉಂದು ಒಂಜಿ ಸಾಫ್ಟ್-ವೇರ್ ದೋಷ ಆದುಪ್ಪು.\nಇಂದೆನ್  [[Special:ListUsers/sysop|ವಿಕಿ-ಅಧಿಕಾರಿಗ್]] ತೆರಿಪಾಲೆ.",
        "missingarticle-rev": "(ಮರು-ಆವೃತ್ತಿ#: $1)",
        "missingarticle-diff": "(ವ್ಯತ್ಯಾಸೊ: $1, $2)",
        "readonly_lag": "ಅಡಿಟುಪ್ಪುನ ಡೇಟಾಬೇಸ್ ಸರ್ವರ್‍ಕುಲು ಮೂಲೊ ಸರ್ವರ್‍ದೊಟ್ಟುಗೆ ಸಮಾನತೆಗ್ ಬರ್ಪುನ ಮುಟ್ಟೊ ಡೇಟಾಬೇಸ್‍ನ್ ಯಾಂತ್ರಿಕವಾದ್ ಮುಚ್ಚಿದ್ ಆತ್ಂಡ್",
+       "nonwrite-api-promise-error": "'Promise-Non-Write-API-Action' ಇನ್ಪಿನ HTTP ಶೀರ್ಷಿಕೆನ್ ಕಡಪುಡುಂಡು ಆಂಡಾ ಕೋರಿಕೆ ಪೋಯಿನಿ ಒಂಜಿ API write module ಗು",
        "internalerror": "ಆ೦ತರಿಕ ದೋಷ",
        "internalerror_info": "ಆಂತರಿಕ ದೋಷ: $1",
        "internalerror-fatal-exception": "\"$1\" ಬಗೆತ ಒಂಜಿ ಗಂಭೀರವಾಯಿನ ಆಕ್ಷೇಪಣೆ",
        "badarticleerror": "ಈ ಪುಟಡ್ ಈರ್ ಪ್ರಯತ್ನಿಸಾನ ಕಾರ್ಯ ಸಾದ್ಯ ಇಜ್ಜಿ",
        "cannotdelete": "ಪುಟ ಅಥವಾ ಹೊತ್ತಗೆ \"$1\" ನ್ ಮಾಜಾವರ ಆಪುಜ್ಜಿ.(ಬೇತೆ ಎರೋ ಮಾಜಾದಿಪ್ಪೆರ್)",
        "cannotdelete-title": "\"$1\" ಮಾಜಾವರೆ ಆಪುಜ್ಜಿ",
+       "delete-scheduled": " \"$1\" ಇನ್ಪಿನ ಪುಟ ಮಾಜಾವರೆ ಸಿದ್ಧವಾದುಂಡು.\n ದಯಮಲ್ತ್ ತಡೆತೊಣುಲೆ.",
        "delete-hook-aborted": "ಮಾಜಪುನೆನ್ ರದ್ದ್ ಮಲ್ತಿನ ಕೊಂಡಿ. ಅವು ಒವ್ವೇ ಇವರಣೆ ಕೊರ್ತ್‌ಜಿ.",
        "no-null-revision": "\"$1\" ಪುಟೊದ ಸೊನ್ನೆ ಪುನರಾವರ್ತನೆನ್ ರಚಿಸಯರ್ ಸಾದ್ಯೊ ಇದ್ದಿ",
        "badtitle": "ಸರಿ ಇಜ್ಜಾಂದಿನ ತರೆಬರವು",
        "badtitletext": "ಈರ್ ಕೇಂಡಿನ ಪುಟೊತ ತರೆಬರವು ಸರಿ ಇಜ್ಜಿ ಅತ್ತ್‌ಡ ಖಾಲಿ ಉಂಡು ಅತ್ತ್‌ಡ ತಪ್ಪು ಕೊಂಡಿಲು ಇತ್ತಿನ ಅಂತರ್ಬಾಸೆ/ಅಂತರ್ವಿಕಿ ತರೆಬರವು ಆದುಪ್ಪು.\nಅಯಿಟ್ ತರೆಬರವುಡು ಗಲಸೆರೆ ಆವಂದಿನಂಚಿತ್ತಿ ಒಂಜಿ ಅತ್ತ್‌ಡ ಜಾಸ್ತಿ ಅಕ್ಷರೊಲು ಉಪ್ಪು.",
        "title-invalid-empty": "ಮನವಿ ಮಾಲ್ತ್‌ನ ಪುಟೊದ ತರೆಬರವು ಕಾಲಿಯಾತ್‍ಂಡ್ ಅತ್ತಂಡ ಕೇವಲೊ ಪುದರ್‍ದ ಜಾಗೆದ ಪುದರ್‍ನ್ ಮಾಂತ್ರೊ ಹೊಂದ್‍ದ್ಂಡ್.",
+       "title-invalid-utf8": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆ ಒಂಜಿ ಅಮಾನ್ಯ  UTF-8  ಅನುಕ್ರಮೊಡು ಉಂಡು.",
+       "title-invalid-interwiki": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆಡ್ ಒಂಜಿ ಅಂತರ-ವಿಕಿ ಕೊಂಡಿ ಉಂಡು.ಅವೆನ್ ಶೀರ್ಷಿಕೆಲೆಡ್ ಬಳಸರೆ ಆಪುಜಿ.",
+       "title-invalid-talk-namespace": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆ ಒಂಜಿ ಇಜ್ಜಾಂತಿನ  ಪಾತೆರಪುಟೊಕು ಉಲ್ಲೇಕೊಂದು ಉಂಡು.",
+       "title-invalid-characters": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆಡ್ ಅಮಾನ್ಯ ಅಕ್ಷರೊಲು ಉಂಡು:\"$1\".",
+       "title-invalid-relative": "ಶೀರ್ಷಿಕೆಡ್ ಸಮ್ಮಂದದ ಸಾದಿ ಉಂಡು.ಸಮ್ಮಂದದ ಪುಟೊತ ಶೀರ್ಷಿಕೆಲು  (./, ../) ಅಮಾನ್ಯವಾದುಂಡು, ದೇಗಿನ್ನಗ ದಿಂಜಸರ್ತಿ ಬಳಕೆದಾರೆರೆನ ಜಾಲಪಟಲೊಡು ಅವೆನ್ ಮುಟ್ಟರೆ ಆಪುಜಿ.",
+       "title-invalid-magic-tilde": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆ ಅಮಾನ್ಯವಾಯಿನ ಮ್ಯಾಜಿಕ್ ಟಿಲ್ಡೆ ಅನುಕ್ರಮ ಉಂಡು.(<nowiki>~~~</nowiki>).",
+       "title-invalid-too-long": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆ ಮಸ್ತ್ ಉದ್ದ ಉಂಡು.ಅವು UTF-8 ಸಂಕೇತಿಕರಣೊಡು $1  -ಇರ್ದ್ {{PLURAL:$1|byte|bytes}}  ಉದ್ದ ಉಪ್ಪರೆ ಬಲ್ಲಿ. .",
+       "title-invalid-leading-colon": "ಕೇಣಿನ ಪುಟೊತ ಶೀರ್ಷಿಕೆದ ಸುರುಟು ಒಂಜಿ ಅಮಾನ್ಯ ಅರ್ಧವಿರಾಮ(ಕೊಲೊನ್) ಚಿನ್ನೆ ಉಂಡು.",
        "perfcached": "ಈ ತಿರ್ತ್‍ದ ಮಾಹಿತಿಲು cacheದ್ ಬತ್ತ್ಂಡ್ ಬುಕ್ಕೊ ಇತ್ತೆದ ಸ್ತಿತಿನ್ ಬಿಂಬಿಸವೊಂದುಂಡು.  ದಿಂಜ ಪಂಡ {{PLURAL:$1|one result is|$1 ಪಲಿತಾಂಸೊಲು}}cacheಡ್ ತಿಕುಂಡು.",
        "perfcachedts": "ಈ ತಿರ್ತ್‍ದ ಮಾಹಿತಿಲು cacheದ್ ಬತ್ತ್ಂಡ್ ಬುಕ್ಕೊ ಇತ್ತೆದ ಸ್ತಿತಿನ್ ಬಿಂಬಿಸವೊಂದುಂಡು.  ದಿಂಜ ಪಂಡ {{PLURAL:$4|one result is|$4 ಪಲಿತಾಂಸೊಲು}}cacheಡ್ ತಿಕುಂಡು.",
        "querypage-no-updates": "ಈ ಪುಟೊತ್ತ ಆಧುನಿಕ ಮಲ್ಪುನ ಕ್ರಿಯೆನ್ ಸದ್ಯಗ್ ಉಂತಾದ್ಂಡ್. ಮುಲ್ಪ ಉಪ್ಪುನ ಮಾಹಿತಿನ್ ನವೀಕರಣ ಮಲ್ಪರಾಪುಜಿ",
        "viewsource": "ಮೂಲೊ ಬರಹೊನು ತೂಲೆ",
        "viewsource-title": " $1 ಮೂಲ ಬರಹ ತೂಲೆ",
        "actionthrottled": "ಕಾರ್ಯನ್ ದೆಂಗಾದುಂಡು",
+       "actionthrottledtext": "ದುರುಪಯೋಗ ತಡೆಪಿನ ಒಂಜಿ ಉದ್ದೇಶೊಡು, ಈರ್ ಅಲ್ಪಾವಧಿಟ್ ದಿಂಜ ಸರ್ತಿ ಉಂದೇ ಕ್ರಿಯೆ ಮಲ್ಪಂದಿಲೆಕ್ಕ ಮಿತಿ ನಿಗದಿ ಮಲ್ದ್ಂಡ್.ಬೊಕ ಈರ್ ಮಿತಿ ದಾಂಟ್'ದರ್.\nದಯಮಲ್ತ್ ಕೆಲ ನಿಮಿಷ ಬುಡುದು ಬೊಕ ಕುಡಾ ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ.",
        "protectedpagetext": "ಈ ಪುಟೊ ಸಂಪೊಲಿಪಂದಿನ ಲೆಕೊ ರಕ್ಸಣೆ ಆತ್ಂಡ್",
        "viewsourcetext": "ಈರ್ ಈ ಪುಟದ ಮೂಲನ್ ತೂವೊಲಿ ಬೊಕ್ಕ ನಕಲ್ ಮಲ್ಪೊಲಿ",
        "viewyourtext": "ಈರ್ ಈ ಪುಟೊದ ಮೂಲನ್ ತೂವೊಲಿ ಬೊಕ್ಕ ನಕಲ್ ಮಲ್ಪೊಲಿ <strong>ಈರೆನ ಸಂಪದನೆ</strong>",
        "protectedinterface": "ಈ ಪುಟೊ ತಂತ್ರಾಂಸೊ ಉಪಯೋಗೊ ಮಲ್ಪುನ ಪಟ್ಯೊನ್ ಒದಗಿಸಾಪುಂಡ್. ದುರುಪಯೋಗ ಅವಂದಿಲೆಕ್ಕ ಇದೆನ್ ರಕ್ಷಣೆ ಮಲ್ಪುಲೆ.\nಮಾತ ವಿಕಿಲೆಗ್ ಬಾಸಾಂತರೊನು ಕೂಡಯೆರೆ ಅಂಚನೆ ಬದಲ್ಪೆರೆ, [https://translatewiki.net/ translatewiki.net], the MediaWiki localisation ಯೋಜನೆನ್ ಉಪಯೊಗಿಸಲೆ\nಕನ್ನಡ",
+       "editinginterface": "<strong>ಎಚ್ಚರಿಕೆ:</strong> ತಂತ್ರಾಂಶೊದ ಅಂತರಮುಖತ ಪಠ್ಯ ಆದಿಪ್ಪುನ ಪುಟೊನು ಈರ್ ಸಂಪಾದಿಸೊಂದು ಉಲ್ಲರ್. ಈ ಪುಟೊತ ಬದಲಾವಣೆಲು ವಿಕಿತ ಇತರೆ ಬಳಕೆದಾರೆರೆಗ್ ತೋಜುನ ಬಳಕೆದಾರೆರ ಅಂತರಮುಖತ ರೂಪೊದ ಮಿತ್ತ್ ಪರಿಣಾಮ ಮಲ್ಪುಂಡು.",
+       "translateinterface": "ಮಾತಾ ವಿಕಿ ಅನುವಾದಲೆಗ್ ಸೇರಾರೆ ಯಾನೆ ಬದಲ್ ಮಲ್ಪರೆ, ದಯಮಲ್ತ್ ಮೀಡಿಯಾವಿಕಿ ಸ್ಥಳೀಕರಣ ಯೋಜನೆ [https://translatewiki.net/ translatewiki.net] ಉಪಯೋಗಿಸಾಲೆ.",
+       "cascadeprotected": "ಈ ಪುಟ ಸಂಪಾದೆರೆ ಆವಂದಿಲೆಕ್ಕ ರಕ್ಷಣೆ ಆತ್ಂಡ್.ದೇಗಿನ್ನಗ,ಉಂದೆನ್ {{PLURAL:$1|ಪುಟ, ...|ಪುಟೊಲು, ....}} ಉಂದೆಟ್ ಅಂತರಸೇರಿಕೆ ಆದ್ ಸೋಪಾನಪಾತ \"cascading\" ಆಯ್ಕೆಡ್ ರಕ್ಷಣೆ ಮಲ್ತ್ಂಡು :$2",
+       "namespaceprotected": "ಇರೆಗ್ <strong>$1</strong> ನಾಮಾವಕಾಶ namespace ದ ಪುಟೊಲೆನ್ ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
+       "customcssprotected": "ಇರೆಗ್ ಈ ಸಿಎಸ್ಎಸ್ CSS ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ,ದೇಗಿನ್ನಗ ಅಯಿಟ್ ನನೊರಿ ಬಳಕೆದಾರೆನ ವೈಯಕ್ತಿಕ ವ್ಯವಸ್ಥೆ ಉಂಡು.",
+       "customjsonprotected": "ಇರೆಗ್ ಈ ಜೆಎಸ್ಒಎನ್ JSON ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ, ದೇಗಿನ್ನಗ ಅಯಿಟ್ ನನೊರಿ ಬಳಕೆದಾರೆನ ವೈಯಕ್ತಿಕ ವ್ಯವಸ್ಥೆ ಉಂಡು.",
+       "customjsprotected": "ಇರೆಗ್ ಈ ಜಾವಾಸ್ಕ್ರಿಪ್ಟ್ Javascript ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ, ದೇಗಿನ್ನಗ ಅಯಿಟ್ ನನೊರಿ ಬಳಕೆದಾರೆನ ವೈಯಕ್ತಿಕ ವ್ಯವಸ್ಥೆ ಉಂಡು.",
+       "sitecssprotected": "ಇರೆಗ್ ಈ ಸಿಎಸ್ಎಸ್ CSS ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ,ದೇಗಿನ್ನಗ ಅಯಿಟ್ ಮಾತಾ ಸಂದರ್ಶಕೆರೆಗ್ ಪರಿಣಾಮ ಆವು.",
+       "sitejsonprotected": "ಇರೆಗ್ ಈ ಜೆಎಸ್ಒಎನ್ JSON ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ,ದೇಗಿನ್ನಗ ಅಯಿಟ್ ಮಾತಾ ಸಂದರ್ಶಕೆರೆಗ್ ಪರಿಣಾಮ ಆವು.",
+       "sitejsprotected": "ಇರೆಗ್ ಈ ಜಾವಾಸ್ಕ್ರಿಪ್ಟ್ Javascript ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ,ದೇಗಿನ್ನಗ ಅಯಿಟ್ ಮಾತಾ ಸಂದರ್ಶಕೆರೆಗ್ ಪರಿಣಾಮ ಆವು.",
+       "mycustomcssprotected": "ಇರೆಗ್ ಈ ಸಿಎಸ್ಎಸ್ CSS ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
+       "mycustomjsonprotected": "ಇರೆಗ್ ಈ ಜೆಎಸ್ಒಎನ್ JSON ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
+       "mycustomjsprotected": "ಇರೆಗ್ ಈ ಜಾವಾಸ್ಕ್ರಿಪ್ಟ್ Javascript ಪುಟೊನು ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
+       "myprivateinfoprotected": "ಇರೆಗ್ ಇರೆನ ಖಾಸಗಿ ಮಾಹಿತಿನ್ ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
+       "mypreferencesprotected": "ಇರೆಗ್ ಇರೆನ ಇಷ್ಟದಾಯ್ಕೆಲೆನ್ ಸಂಪಾದೆರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
        "ns-specialprotected": "ವಿಶೇಷ ಪುಟ‘ಕ್‘ಲೆನ್ ಸಂಪಾದನೆ ಮಲ್ಪರೆ ಆಪುಜಿ",
+       "titleprotected": "ಈ ಶೀರ್ಷಿಕೆ ಸೃಷ್ಟಿ ಆವಂದಿನಂಚ  [[User:$1|$1]] ರಕ್ಷಣೆ ಮಲ್ದೆರ್. ಆರ್ ಕೊರಿನ ಕಾರಣ:\n<em>$2</em>.",
+       "filereadonlyerror": "$1 ಕಡತೊನು ಮಾರ್ಪಾಟು ಮಲ್ಪರೆ ಆಪುಜಿ ದೇಗಿನ್ನಗ,ಕಡತ ಸಂಪುಟ $2 ಓದರೆ-ಮಾತ್ರಾ ರೀತಿಡ್ ಉಂಡು.\nಬೀಗಪಾಡಿನ ಗಣವ್ಯೂಹ ನಿರ್ವಾಹಕೆರ್ ಇಂಚ ವಿವರಣೆ ಕೊರ್ತೆರ್:$3",
+       "invalidtitle": "ಅಮಾನ್ಯ ತರೆಬರವು",
+       "invalidtitle-knownnamespace": "ನಾಮಾವಕಾಶ $2 ಬೊಕ ಪಠ್ಯ $3 ದೊಟ್ಟುಗು ಅಮಾನ್ಯವಾಯಿನ ತರೆಬರವು.",
+       "invalidtitle-unknownnamespace": "ಗೊತ್ತಾಂತಿ ನಾಮಾವಕಾಶ ಸಂಕೆ $1 ಬೊಕ ಪಠ್ಯ $2 ದೊಟ್ಟುಗು ಅಮಾನ್ಯ ತರೆಬರವು",
        "exception-nologin": "ಲಾಗಿನ್ ಆತ್‘ಜ್ಜರ್",
+       "exception-nologin-text": "ಈ ಪುಟ ಇಜಿಂಡ ಕ್ರಿಯೆನ್ ತೂವರೆ ದಯಮಲ್ತ್ ಉಳಪ್ರವೇಶ (ಲಾಗಿನ್) ಮಲ್ಪುಲೆ",
+       "exception-nologin-text-manual": "ಈ ಪುಟ ಇಜಿಂಡ ಕ್ರಿಯೆ ತೂವರೆ ದಯಮಲ್ತ್ $1",
+       "virus-badscanner": "ತಪ್ಪು ಸಂರಚನೆ: ಗೊತ್ತಾಂತಿನ ವೈರಾಣು ಶೋಧಕ:<em>$1</em>",
        "virus-scanfailed": "ಸ್ಕಾನ್ ಅಯಿಜಿ(code $1)",
        "virus-unknownscanner": "ಗುರ್ತದಾಂತಿ antivirus:",
        "logouttext": "<strong>ಈರ್ ಇತ್ತೆ ಲಾಗ್ ಔಟ್ ಆತರ್</strong>\nಗಮನಿಸಲೆ ಈರೆನ ಬ್ರೌಸರ್‍ದ cacheನ್ ದೆತ್ತ ಪಾಡುನೆಟ ಮುಟ್ಟೊ ಕೆಲವು ಪುಟೊಲು ಈರ್ ನಾನಲ ಲಾಗ್ ಇನ್ ಆದಿಪ್ಪುಂಚ ತೋಜುಂಡು.",
        "cannotlogin-title": "ಇತ್ತೆ ಉಲಾಯಿ ಪೋಯರ್ ಸಾದ್ಯೊ ಅವೊಂತಿಜ್ಜಿ",
        "cannotlogin-text": "ಲಾಗ್ ಇನ್ ಅಯಾರ ಅವೊಂತಿಜ್ಜಿ.",
        "cannotloginnow-title": "ಇತ್ತೆ ಉಲಾಯಿ ಪೋಯರ್ ಸಾದ್ಯೊ ಇದ್ದಿ",
+       "cannotloginnow-text": "$1 ಗಳಸುನಗ ಉಳಪ್ರವೇಶ ಸಾಧ್ಯ ಇಜ್ಜಿ.",
        "cannotcreateaccount-title": "ಕಾತೆ ನಿರ್ಮಾಣೊ ಮಲ್ಪೆರೆ ಆವೊಂತಿಜ್ಜಿ",
+       "cannotcreateaccount-text": "ಸೀದಾ ಖಾತೆ ರಚನೆ ಈ ವಿಕಿಟ್ ಅನು ಮಲ್ದಿಜಿ.",
        "yourdomainname": "ಈರೆನ ಕಾರ್ಯಕ್ಷೇತ್ರ",
        "password-change-forbidden": "ಈರ್ ಈ ವಿಕಿಡ್ ಪ್ರರವೇಸ ಪದೊನು ಬದಲ್ಪೆರೆ ಸಾದ್ಯೊ ಇದ್ದಿ.",
+       "externaldberror": "ಒಂಜಾ ದತ್ತಸಂಚಯೊಡು ಒಂಜಿ ಪ್ರಾಮಾಣಿಕೆದ ದೋಷ ಆದುಪ್ಪೊಡು, ಇಜಿಂಡ  ಇರೆನ ಬಾಹ್ಯ ಖಾತೆನ್ ಇರೆಗ್ ಕಾಲನವಿ ಮಲ್ಪರೆ ಅನುಮತಿ ಇಜ್ಜಿ.",
        "login": "ಲಾಗಿನ್ ಆಲೆ",
        "login-security": "ಇರೆನಾ ಗುರ್ತನ್ ಪರಿಸೆ ಮಾಂಪುಲೆ",
        "nav-login-createaccount": "ಲಾಗ್-ಇನ್ / ಅಕೌಂಟ್ ಸೃಷ್ಟಿ ಮಲ್ಪುಲೆ",
        "createaccount": "ಪೊಸ ಖಾತೆ ಸುರು ಮಲ್ಪುಲೆ",
        "userlogin-resetpassword-link": "ಇರೆನೆ ಪ್ರವೇಸೊ ಪದೊನು ಮರತ್ತ್‌‌ದರೆ?",
        "userlogin-helplink2": "ಲಾಗಿನ್ ಆಯೆರೆ ಸಹಾಯೊ",
+       "userlogin-loggedin": "ಈರ್ ಅದಗನೆ {{GENDER:$1|$1}}ಆದ್ ಉಳಪ್ರವೇಶ ಮಲ್ದರ್.ಬೇತೆ ಬಳಕೆದಾರೆ ಆದ್ ಉಳಪ್ರವೇಶೊಗು ತಿರ್ತುದ ಪ್ರಪತ್ರೊನು ಬಳಸುಲೆ.",
+       "userlogin-reauth": "ಈರೇ {{GENDER:$1|$1}} ಇಂದ್ ಸತ್ಯಾಪನೆಗಾದ್, ಈರ್  ಕುಡಾ ಉಳಪ್ರವೇಶ ಮಲ್ಪೊಡು.",
        "userlogin-createanother": "ಪೊಸ ಕಾತೆ ಸುರು ಮಲ್ಪುಲೆ",
        "createacct-emailrequired": "ಇ-ಅಂಚೆ ವಿಳಾಸೊ",
        "createacct-emailoptional": "ಮಿಂಚಂಚೆ ವಿಲಾಸೊ(ಐಚ್ಛಿಕೊ)",
        "createacct-email-ph": "ಇರೆನ ಮಿಂಚಂಚೆ ವಿಲಾಸೊನ್ ಬರೆಲೆ.",
        "createacct-another-email-ph": "ಇ-ಅಂಚೆ ವಿಳಾಸೊನು ಬದಲಾವಣೆ ಮಲ್ಪುಲೆ",
        "createaccountmail": "(ರಾಂಡಮ್) ತಾತ್ಕಾಲಿಕವಾದ್ ಯಾದೃಚ್ಛಿಕ ಪಾಸ್ವರ್ಡ್ ಆಯ್ಕೆ ಮಾಲ್ಪುಲೆ ಬುಕ್ಕೊ ಇಮೇಲ್ ವಿಳಾಸೊನು ಸೂಚಿಸದ್ : ಕಡಪುಡುಲೆ",
+       "createaccountmail-help": "ಸಂಕೇತಪದ ಕಲ್ಪಂದೆ ನನೊರಿ ವ್ಯಕ್ತಿಗ್ ಖಾತೆ ದೆಪ್ಪರೆ ಬಳಸೊಲಿ.",
        "createacct-realname": "ನಿಜವಾಯಿನ ಪುದರ್(ಐಚ್ಛಿಕೊ)",
        "createacct-reason": "ಕಾರಣೊ",
        "createacct-reason-ph": "ಈರ್ ದಾಯೆ ನಾನಲ ಒಂಜಿ ಕಾತೆ ದೆತ್ತೊಂದುಲ್ಲರ್?",
+       "createacct-reason-help": "ಖಾತೆ ರಚನೆದ ಚರಿಟ್ ಸಂದೇಶ ತೋಜಾದುಂಡು.",
        "createacct-submit": "ಪೊಸ ಕಾತೆ ಸುರು ಮಲ್ಪುಲೆ",
        "createacct-another-submit": "ಪೊಸ ಕಾತೆ ಸುರು ಮಲ್ಪುಲೆ",
+       "createacct-continue-submit": "ಖಾತೆ ರಚನೆ ದುಂಬರಿಲೆ.",
+       "createacct-another-continue-submit": "ಖಾತೆ ರಚನೆ ದುಂಬರಿಲೆ.",
        "createacct-benefit-heading": "{{SITENAME}} ನಿಕ್ಲೆನಂಚಿತ್ತಿನ ಎಡ್ದೆಂತಿನಕ್ಲೆಡ್ದ್ ಉಂಡಾತ್‍ಂಡ್.",
        "createacct-benefit-body1": "{{PLURAL:$1|ಸಂಪಾದನೆ|ಸಂಪಾದನೆಲು}}",
        "createacct-benefit-body2": "{{PLURAL:$1|ಪುಟೊ|ಪುಟೊಕುಲು}}",
        "createacct-benefit-body3": "ಇಂಚಿಪೊ{{PLURAL:$1|ಕಾನಿಕೆ ಕೊರಿನಾರ್|ಕಾನಿಕೆ ಕೊರಿನಕುಲು}}",
        "badretype": "ಈರ್ ಕೊರ್ನ ಪ್ರವೇಶ ಪದೆ ಬೇತೆ ಬೇತೆ ಅತ್ಂಡ್",
+       "usernameinprogress": "ಈ ಬಳಕೆದಾರೆ ಪುದರುದ ಖಾತೆ ರಚನೆ ಪ್ರಗತಿಡು ಉಂಡು. ದಯಮಲ್ತ್ ವಂತೆ ಕಾಪುಲೆ.",
        "userexists": "ಈರ್ ಕೊರ್ನ ಸದಸ್ಯರ ಪುದರ್ ಬಳಕೆಡ್ ಉಂಡು. ದಯದೀದ್ ಬೇತೆ ಪುದರ್ ಕೊರ್ಲೆ",
        "loginerror": "ಲಾಗಿನ್ ದೋಷ",
        "createacct-error": "ಕಾತೆ ನಿರ್ಮಾಣೊ ದೋಸೊ",
        "nocookiesnew": "ಈರೆನ ಬಳಕೆದಾರ ಖಾತೆ ಸೄಷ್ಟಿ ಅತ್ಂಡ್. ಆಂಡ ಈರ್ ಲಾಗ್ ಇನ್ ಆತ್‘ಜ್ಜರ್.\n{{SITENAME}} ಲಾಗ್ ಇನ್ ಮಲ್ಪರ ಕುಕೀಸ್ ಉಪಯೋಗ ಮಲ್ಪುಂಡ್.\nಈರೆನ ಗಣಕಯಂತ್ರಡು ಕುಕೀಸ್ ನಿಷಿದ್ದ ವಾತ್ಂಡ್.\nದಯದೀದ್ ಈ ನಿಷಿದ್ಧನ್ ದೆತ್ತ್‘ದ್, ಈರೆನ ಬಳಕೆದಾರ ಪುದರ್ ಬೊಕ್ಕ  ಪ್ರವೇಶಪದ ಉಪಯೋಗಿಸಾದ್ ಲಾಗ್ ಇನ್ ಆಲೆ.",
        "nocookieslogin": "{{SITENAME}} ಲಾಗ್ ಇನ್ ಮಲ್ಪರ ಕುಕೀಸ್ ಉಪಯೋಗ ಮಲ್ಪುಂಡ್.\nಈರೆನ ಗಣಕಯಂತ್ರಡು ಕುಕೀಸ್ ನಿಷಿದ್ದ ವಾತ್ಂಡ್.\nದಯದೀದ್ ಈ ನಿಷಿದ್ಧನ್ ದೆತ್ತ್‘ದ್, ಕುಡೊರ ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ.",
        "nocookiesfornew": "ಮೂಲನ್  ನಿರ್ಧರಿಸಾವರ ಆವೊಂದಿಜ್ಜಿ  ಐಡ್‘ದಾವರ  ಈರೆನ ಖಾತೆ ಸೃಷ್ಟಿ ಆತ್‘ಜ್ಜಿ.  .\nದಯದೀದ್ ಕುಕೀಸ್ ನಿಷಿದ್ಧನ್ ದೆತ್ತ್‘ದ್, ಈ ಪುಟನ್ ಪಿರ ದಿಂಜಾದ್ (load)ಪ್ರಯತ್ನಿಸಾಲೆ.",
+       "createacct-loginerror": "ಖಾತೆ ರಚನೆ ಸಫಲ ಆಂಡ್, ಆಂಡಾ ಇರೆನ ಸ್ವಯಂ ಉಳಪ್ರವೇಶ ಮಲ್ಪರೆ ಆತಿಜಿ. ದಯಮಲ್ತ್ [[Special:UserLogin|manual login]] ಗು ದುಂಪೋಲೆ.",
        "noname": "ಈರ್ ಸರಿಯಾಯಿನ ಬಳಕೆದಾರ ಪುದರ್ ಕೊರ್ತಿಜ್ಜರ್.",
        "loginsuccesstitle": "ಲಾಗ್ ಇನ್ ಯಶಸ್ವಿಯಾತ್ಂಡ್",
        "loginsuccess": "ಲಾಗ್ ಇನ್ ಯಶಸ್ವಿಯಾತ್‘ಂಡ್\". {{SITENAME}}  \"$1\".'''",
        "wrongpasswordempty": "ಖಾಲಿ ಪ್ರವೇಶ ಪದ ಕೊರ್ತರ್. ನನ ಒರ ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ.",
        "passwordtooshort": "ಪ್ರವೇಶ ಪದಟ್ ಕನಿಷ್ಟ {{PLURAL:$1|೧ ಅಕ್ಷರ|$1 ಅಕ್ಷರರೊಳೆನ್}} ಉಪ್ಪೊಡ್",
        "passwordtoolong": "ಪ್ರವೇಸೊ ಪದೊಟು ಕನಿಸ್ಟೊ {{PLURAL:$1|೧ ಅಕ್ಷರ|$1 ಅಕ್ಷರರೊಲು}} ಉಪ್ಪೊಡ್",
+       "passwordtoopopular": "ಸಾಮಾನ್ಯ ಆಯ್ಕೆದ ಸಂಕೇತಪದೊಲೆನ್ ಬಳಕೆ ಮಲ್ಪರೆ ಆವಂದ್. ದಯಮಲ್ತ್ ಕಲ್ಪನೆಗ್ ಎಚ್ಚ ಬಂಗ ಆಪಿನ ಸಂಕೇತಪದೊನು ಆಯಿಲೆ.",
        "passwordinlargeblacklist": "ಸೇರಾಯಿನ ಪ್ರವೇಶಪದ ಅತಿ ಸಾಮಾನ್ಯವಾದ್ ಬಳಕೆ ಮಲ್ಪುನ ಪ್ರವೇಶಪದೊಕುಲೆನ ಒಂಜಿ ಪಟ್ಟಿಡ್ ಉಂಡು.ದಯಮಲ್ತ್ ಬೇತೆ ಅನನ್ಯ ಪ್ರವೇಶಪದೊನು ಆಯ್ಕೆ ಮಲ್ಪುಲೆ.",
        "password-name-match": "ಇರೆನ್ ಪ್ರವೇಶಪದ ಬಳಕೆದಾರೆನ ಪುದರ್‘ಡ್‘ದ್ ಬೇತೆ ಉಪ್ಪೊಡು",
        "password-login-forbidden": "ಈ ಪ್ರವೇಶಪದ ಬೊಕ್ಕ ಬಳಕೆದಾರೆರೆನ ಪುದರ್‘ನ್ ನಿಷಿದ್ಧ ಮಲ್ತ್‘ದ್ಂಡ್",
        "mailmypassword": "ಪ್ರವೇಸೊ ಪದೊನ್ ಪಿರ ಸ್ತಾಪನೆ ಮಲ್ಪುಲೆ",
        "passwordremindertitle": "{{SITENAME}}ಗ್ ಪೊಸ ತಾತ್ಕಾಲಿಕ ಪ್ರವೇಶ ಪದ",
+       "passwordremindertext": "ಏರಾಒರಿ ($1 ಐಪಿ ವಿಳಾಸೊರ್ದು) {{ಜಾಲತಾಣಪುದರು}}$4ಗು ಒಂಜಿ ಪೊಸ ಸಂಕೇತಪದ ಕೇಂಡೆರ್. ಬಳಕೆದಾರೆ $2 ಗು ಒಂಜಿ ತತ್ಕಾಲದ ಸಂಕೇತಪದ ರಚಿಸಾದ್ ಬೊಕ ಅವೆನ್ $3 ಮಲ್ತ್ಂಡ್.ಉಂದು ಇರೆನ ಬಯಕೆ ಆದಿತ್ತ್ಂಡ, ಈರ್ ಉಳಪ್ರವೇಶ ಮಲ್ತ್ ಬೊಕ ಪೊಸ ಸಂಕೇತಪದ ಆಯ್ಕೆ ಮಲ್ಪೊಡು.ಇರೆನ ತತ್ಕಾಲದ ಸಂಕೇತಪದ {{PLURAL:$5|ಒಂಜಿದಿನ|$5 ದಿನೊಟು}}ಮುಗಿವುಂಡು.\nಒಂಜಾ ಬೇತೆ ಏರಾಒರಿ ಈ ಕೋರಿಕೆ ಮಲ್ತಿನಾತ್ಂಡ, ಇಜಿಂಡ ಈರೆಗ್ ಇರೆನ ದುಂಬುದ ಸಂಕೇತಪದ ನೆನಪಾಂಡ, ಬೊಕ ಈರ್ ಅವೆನ್ ಬದಲಾರೆ ಬಯಕುಜರ್ಡ, ಈರ್ ಈ ಸಂದೇಶೊನು ಅವಗಣನೆ ಮಲ್ಪೊಲಿ ಬೊಕ ಇರೆನ ಪರ ಸಂಕೇತಪದೊ ಬಳಕೆನ್ ದುಂಬರಿಯೊಲಿ.",
        "noemail": "ಸದಸ್ಯೆ \"$1\" ಪುದರ್‘ಡ್ ವಾ ಇ-ಅಂಚೆ ವಿಳಾಸೊಲ ದಾಖಲಾತ್‘ಜ್ಜಿ",
        "noemailcreate": "ಈರ್ ಇತ್ತೆ ಉಪ್ಪುನ ಇ-ಅಂಚೆ ವಿಳಾಸ ಕೊರೊಡ್",
        "passwordsent": "\"$1\" ಇಂಬೆರೆನ ಪುದರ್‘ಡ್ ದಾಖಲಾತ್‘ನ ಇ-ಅಂಚೆ ವಿಳಾಸೊಗ್ ಪೊಸ ಪ್ರವೇಶಪದ ಕಡಪುಡ್‘ದುಂಡು.ಐನ್ ತೂದು ಬೊಕ್ಕ ಕುಡ ಲಾಗಿನ್ ಇನ್ ಆಲೆ.",
+       "blocked-mailpassword": "ಇರೆನ ಐಪಿ ವಿಳಾಸೊನು ಸಂಪಾದೆರೆ ಆವಂದಿನಂಚ ತಡೆತುದುಂಡು. ದುರುಪಯೋಗ ಆವರೆ ಬಲ್ಲಿ ಇಂದ್ ಈ ಐಪಿ ವಿಳಾಸೊಡು ಸಂಕೇತಪದ ಪಿರಪಡೆಪಿನ ಸೌಲಭ್ಯೊನು ಉಂತಾದ್ಂಡ್.",
+       "eauthentsent": "ಒಂಜಿ ದೃಡೀಕರಣ ಇ-ಅಂಚೆನ್ ವಿಸೂಚಿತ ಇಅಂಚೆಗ್ ಕಡಪುಡುದುಂಡು.ಖಾತೆಗ್ ಬೇತೆ ಒವ್ವೆ ಇಅಂಚೆ ಕಡಪುಡುನ ದುಂಬು, ಖಾತೆ ಇರೆನವೆ ಇಂದ್ ಖಾತ್ರಿ ಮಲ್ಪರೆ, ಈರ್ ಇ-ಅಂಚೆಡ್ ಪಣ್ತಿನ ಸೂಚನೆಲೆನ್ ಅನುಸರಿಸಾವೊಡು.",
+       "throttled-mailpassword": "ಕರಿನ {{PLURAL:$1|ಗಂಟೆ|$1ಗಂಟೆಲೆಡ್}} ಒಂಜಿ ಸಂಕೇತಪದ ಪಿರಸ್ಥಾಪನ ಇಮೇಲ್ ಕಡಪುಡುದುಂಡು.\nದುರುಪಯೋಗ ಆವರೆಬಲ್ಲಿಂದ್,ಒಂಜೆ ಒಂಜಿ ಪಿರಸ್ಥಾಪನ ಸಂಕೇತಪದ ಇಮೇಲ್ {{PLURAL:$1|ಗಂಟೆ|$1ಗಂಟೆಲು}}ಕಡಪುಡುಂಡು.",
        "mailerror": "ಅಂಚೆ ಕಡಪುಡುನೆಡ್ ದೋಸೊ: $1",
+       "acct_creation_throttle_hit": "ಈ ವಿಕಿತ ಸಂದರ್ಶಕೆರ್ ಇರೆನ ಐಪಿ ವಿಳಾಸ ಬಳಸಾದ್ {{PLURAL:$1|ಖಾತೆ|$1ಖಾತೆಲೆನ್}} ಕರಿನ $2ಡು ರಚಿಸಾದೆರ್.ಅವು ಈ ಅವಧಿಡ್ ಅನುಮತಿ ಕೊರ್ತಿನ ಗರಿಷ್ಟಮಿತಿ. ಅಯಿತ ಫಲವಾದ್,ಈ ಐಪಿ ವಿಳಾಸ ಗಳಸುನ ಸಂದರ್ಶಕೆರ್ ನನ ಹೆಚ್ಚಿನ ಖಾತೆಲೆನ್ ಇತ್ತೆ ದೆಪ್ಪರೆ ಆಪುಜಿ.",
        "emailauthenticated": "ಇರೆನ ಈ-ಮೈಲ್ ವಿಲಾಸೊ $2 ತಾನಿ $3 ಕ್ಕ್ ಒಪ್ಪಿಗೆ ಆತ್ಂಡ್.",
+       "emailnotauthenticated": "ಇರೆನ ಇಮೇಲ್ ವಿಳಾಸ ನನಲಾ ದೃಡೀಕೃತ ಆತಿಜಿ.\nಈ ತಿರ್ತದ ಒವ್ವೆ ಲಕ್ಷಣೊಲೆಗ್ ಇಮೇಲ್ ಕಡಪುಡರೆ ಆಪುಜಿ.",
        "noemailprefs": "ಈ ಸೌಲಭ್ಯೊಲ್ ಕೆಲಸ ಮಲ್ಪರೆ ಒಂಜಿ ಇ-ಅಂಚೆ ವಿಳಾಸೊನ್ ನಮೂದು ಮಲ್ಪುಲೆ.",
        "emailconfirmlink": "ಇರೆನ ಇ-ಅಂಚೆ ವಿಳಾಸೊನ್ ದೃಡೀಕರಣ ಮಲ್ಪುಲೆ.",
+       "invalidemailaddress": "ಇಮೇಲ್ ವಿಳಾಸ ಅಮಾನ್ಯ ರೂಪನೊಡು ಉಪ್ಪುನಂಚ ತೋಜುನ ಕಾರಣ ಒಪ್ಪೆರೆ ಅಪುಜಿ.\nದಯಮಲ್ತ್ ಸರಿ-ರೂಪನದ ವಿಳಾಸ ಸೇರಾಲೆ,ಇಜಿಂಡ ಆ ಕಳೊನು ಖಾಲಿ ಬುಡುಲೆ.",
+       "cannotchangeemail": "ಖಾತೆ ಇಮೇಲ್ ವಿಳಾಸೊಲೆನ್ ಈ ವಿಕಿಟ್ ಬದಲರೆ ಆಪುಜಿ",
        "emaildisabled": "ಈ ಜಾಲತಾಣಡ್‍ದ್ ಮಿಂಚಂಚೆ ಕಡಪುಡರ ಆಪುಜ್ಜಿ",
        "accountcreated": "ಖಾತೆ ಸೃಷ್ಟಿಯಾತ್‘ಂಡ್.",
+       "accountcreatedtext": "ಬಳಕೆದಾರೆ ಖಾತೆ[[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|talk]]) ಗಾದ್ ರಚಿಸಾದ್ಂಡ್.",
+       "createaccount-title": "ಖಾತೆ ರಚನೆ {{SITENAME}}ಗಾದ್",
+       "createaccount-text": "ಏರಾಒರಿ ಇರೆನ ಇಮೆಲ್ ವಿಳಾಸೊಗು ಒಂಜಿ ಖಾತೆ ರಚಿಸಾದೆರ್ {{SITENAME}} ($4) ಪುದರ್ \"$2\", ಸಂಕೇತಪದ\"$3\".\nಈರ್ ಉಳಪ್ರವೇಶ ಮಲ್ತ್ ಬೊಕ  ಇತ್ತೆನೇ ಸಂಕೇತಪದ ಬದಲಾಲೆ.\nಈ ಖಾತೆ ದೋಷೊಡು ರಚನೆ ಆತಿನಾಂಡ,ಈರ್ ಈ ಸಂದೇಶೊನು ನಿರ್ಲಕ್ಷ ಮಲ್ಪೊಲಿ.",
+       "login-throttled": "ಈರ್ ದಿಂಜ ಸರ್ತಿ ಉಳಪ್ರವೇಶ ಪ್ರಯತ್ನ ಮಲ್ದರ್.\nದಯಮಲ್ತ್  ನನೊರ ಪ್ರಯತ್ನ ಮಲ್ಪುನ ದುಂಬು $1 ಕಾಪುಲೆ.",
        "login-abort-generic": "ಇರೆನ ಲಾಗ್ ಇನ್ ಪೈಲ್ ಆತ್ಂಡ್",
+       "login-migrated-generic": "ಇರೆನ ಖಾತೆ ವಲಸೆ ಆತ್ಂಡ್,ಬೊಕ ಇರೆನ ಬಳಕೆಪುದರ್ ಈ ವಿಕಿಟ್ ಅಸ್ತಿತ್ವೊಡು ಇಜ್ಜಿ.",
        "loginlanguagelabel": "ಬಾಸೆ: $1",
+       "suspicious-userlogout": "ಇರೆನ ಪಿರಗಮನ ಕೋರಿಕೆನ್ ನಿರಾಕರಿಸಾದ್ಂಡ್ ದೇಗಿನ್ನಗ ಅವು ಒಂಜಿ ಕಡಿದಿನ ಜಾಲದರ್ಶಿ ಇಜಿಂಡ ಸಂಗ್ರಹಿತ ನಿಕಟಪ್ರತಿ ಕಡಪುಡುದಿನ ಇಂದ್ ತೋಜುಂಡು",
+       "createacct-another-realname-tip": "ನಿಜವಾಯಿನ ಪುದರ್ ಐಚ್ಚಿಕ.\nಈರ್ (ಪುದರ್)ಕೊರೊಡುಂದು ಆಯ್ಕೆ ಮಲ್ತರ್ಡ,ಅವೆನ್ ಬಳಕೆದಾರೆಗ್ ಕಾರ್ಯಪ್ರಶಸ್ತಿ ಕೊರಿಯರೆ ಗಳಸುಂಡು.",
        "pt-login": "ಲಾಗ್ ಇನ್",
        "pt-login-button": "ಲಾಗಿನ್ ಆಲೆ",
        "pt-login-continue-button": "ಲಾಗಿನ್ ಅದ್ ಮುಂದುವರಿಲೆ",
        "pt-createaccount": "ಪೊಸ ಖಾತೆ ಸುರು ಮಲ್ಪುಲೆ",
        "pt-userlogout": "ಲಾಗ್ ಔಟ್",
+       "php-mail-error-unknown": " PHP ದ mail() ಕಾರ್ಯೊಡು ಗೊತ್ತಾಂತಿನ ದೋಷ.",
+       "user-mail-no-addy": "ಇಮೇಲ್ ವಿಳಾಸ ದಾಂತೆ ಇಮೇಲ್ ಕಡಪುಡರೆ ಯತ್ನ ಮಲ್ತ್ಂಡ್.",
+       "user-mail-no-body": "ಖಾಲಿ ಇಜಿಂಡ ಅನುಚಿತ ಕುದ್ಯಕಾಯದ ಇಮೇಲ್ ಕಡಪುಡೆರೆ ಯತ್ನ ಮಲ್ತ್ಂಡ್.",
        "changepassword": "ಪ್ರವೇಶಪದೊನ್ ಬದಲಾವಣೆ ಮಲ್ಪುಲೆ",
+       "resetpass_announce": "ಉಳಗಮನ ಮುಗಿಸಾರೆ,ಈರ್ ಒಂಜಿ ಪೊಸ ಸಂಕೇತಪದ ಜೋಡಿಸಾವೊಡು.",
        "resetpass_header": "ಈ ಖಾತೆದ ಪ್ರವೇಶಪದ ಬದಲಾವಣೆ ಮಲ್ಪುಲೆ",
        "oldpassword": "ಪರ ಪ್ರವೇಶಪದ",
        "newpassword": "ಪೊಸ ಪ್ರವೇಶಪದ",
        "retypenew": "ಪ್ರವೇಶಪದ ಪಿರ ಟೈಪ್ ಮಲ್ಪುಲೆ",
        "resetpass_submit": "ಪ್ರವೇಶಪದ ನಿಶ್ಚಯ ಮಲ್ತ್‘ದ್ ಲಾಗ್ ಇನ್ ಆಲೆ",
        "changepassword-success": "ಈರೆನ ಪಾಸ್‍ವರ್ಡ್ ಬದಲಾತ್‍ಂಡ್",
+       "changepassword-throttled": "ಈರ್ ದಿಂಜ ಸರ್ತಿ ಉಳಪ್ರವೇಶ ಪ್ರಯತ್ನ ಮಲ್ದರ್.\nದಯಮಲ್ತ್  ನನೊರ ಪ್ರಯತ್ನ ಮಲ್ಪುನ ದುಂಬು $1 ಕಾಪುಲೆ.",
        "botpasswords": "ಬಾಟ್ ಪಾಸ್‍ವರ್ಡ್",
+       "botpasswords-summary": "<em>Bot passwords</em> ಖಾತೆದ ಮುಖ್ಯ ಉಳಗಮನ ರುಜುವಾತುಲೆನ್ ಬಳಸಂದೆ  API ಮೂಲಕ ಬಳಕೆದಾರ ಖಾತೆಗ್ ಪ್ರವೇಶ ಕೊರ್ಪುಂಡು.ಬಾಟ್ ಸಂಕೇತಪದ ಮೂಲಕ ಉಳಗಮನ ಮಲ್ತಿನದಗ ಬಳಕೆದಾರೆರ ಹಕ್ಕುಲು ಮಿತವಾದುಪ್ಪು.\nಇರೆಗ್ ಉಂದೆನ್ ದಾಯೆಗ್ ಮಲ್ಪೊಡು ಇಂದ್ ಗೊತ್ತಿಜ್ಜಿಡ,ಈರ್ ಅವೆನ್ ಮಲ್ಪಂದೆ ಉಪ್ಪುನು ಎಡ್ಡೆ.ಏರ್ಲಾ ಇರೆಗ್ ಏಪಲಾ ಇಂಚಿನ ಒಂಜೆನ್ ಪುಟ್ಟಾದ್ ಅಕಲೆಗ್ ಕೊರುಲೆಂದ್ ಇರೆಗ್ ಕೇಣರೆ ಬಲ್ಲಿ.",
        "botpasswords-disabled": "ಬಾಟ್ ಪಾಸ್‍ವರ್ಡ್‍ಲೆನ್ ನಿಸ್ಕ್ರೀಯೊ ಮಲ್ತಾತ್ಂಡ್",
+       "botpasswords-no-central-id": "ಬಾಟ್ ಸಂಕೇತಪದೊಲೆನ್ ಬಳಕೆ ಮಲ್ಪರೆ ಈರ್ ಒಂಜಿ ಕೇಂದ್ರಿಕೃತ ಖಾತೆಗ್ ಉಳಗಮನ ಆದಿಪ್ಪೊಡು.",
        "botpasswords-existing": "ಅಸ್ತಿತ್ವೊಡು ಇದ್ಯಾಂದಿನ ಬಾಟ್ ಪಾಸ್‍ವರ್ಡ್",
+       "botpasswords-createnew": "ಪೊಸ ಬಾಟ್ ಸಂಕೇತಪದ ರಚಿಸಾಲೆ.",
+       "botpasswords-editexisting": "ಉಪ್ಪುನ ಒಂಜಿ ಸಂಕೇತಪದೊನು ಸಂಪಾದಾಲೆ.",
+       "botpasswords-label-needsreset": "(ಸಂಕೇತಪದ ತಿರುಜೋಡನೆ ಆವೊಡು)",
        "botpasswords-label-appid": "ಬಾಟ್ ಪುದರ್",
        "botpasswords-label-create": "ಸ್ರಿಸ್ಟಿಸಲೆ",
        "botpasswords-label-update": "ಮಿತ್ತ್ ಏರಲೆ",
        "botpasswords-label-delete": "ದೆಪ್ಪುಲೆ",
        "botpasswords-label-resetpassword": "ಪ್ರವೇಸೊ ಪದೊನ್ ಪಿರ ಸ್ತಾಪನೆ ಮಲ್ಪುಲೆ",
        "botpasswords-label-grants": "ಅನ್ವಯೊ ಆಪುನ ಅನುದಾನೊ",
+       "botpasswords-help-grants": "ಅನುದಾನೊಲು ಇರೆನ ಬಳಕೆದಾರೆ ಖಾತೆಡ್ ಇತ್ತೆ ಉಪ್ಪುನ ಹಕ್ಕುಲೆಗ್ ಪ್ರವೇಶ ಕೊರ್ಪುಂಡು. ಒಂಜಿ ಅನುದಾನೊನು ಮುಲ್ಪ ಸಶಕ್ತ ಮಲ್ತ್ಂಡ,ಅವು ಇತ್ತೆ ಇರೆನ ಬಳಕೆದಾರೆ ಖಾತೆಡ್ ಉಪ್ಪಂದಿನ  ಹಕ್ಕುಲೆಗ್ ಪ್ರವೇಶ ಕೊರ್ಪುಜಿ.ಹೆಚ್ಚಿನ ಮಾಹಿತಿಗಾದ್ \n[[Special:ListGrants|table of grants]] ತೂಲೆ.",
        "botpasswords-label-grants-column": "ಅನುದಾನೊ",
+       "botpasswords-bad-appid": "ಬಾಟ್ದ ಪುದರ್ $1 ಮಾನ್ಯ ಅತ್ತ್.",
+       "botpasswords-insert-failed": "ಬಾಟ್ದ ಪುದರ್ $1 ಸೇರಾರೆ ವಿಫಲವಾಂಡ್.ಅವು ಅದಗನೇ ಸೇರ್ದುಂಡಾ?",
+       "botpasswords-update-failed": "ಬಾಟ್ದ ಪುದರ್ $1 ಕಾಲನವಿಕ ವಿಫಲವಾಂಡ್.ಅವೆನ್ ಮಾಜಾದಿಂಡಾ?",
        "botpasswords-created-title": "ಬಾಟ್ ಪಾಸ್‍ವರ್ಡ್‌ನ್ ಸ್ರಿಸ್ಟಿಸದಾತ್ಂಡ್",
+       "botpasswords-created-body": "$1 ಬಾಟ್ ಪುದರ್ದ {{GENDER:$2|ಬಳಕೆದಾರೆ}}ಬಾಟ್ ಸಂಕೇತಪದ $2 ರಚನೆ ಆತ್ಂಡ್.",
        "botpasswords-updated-title": "ಬಾಟ್ ಪಾಸ್‍ವರ್ಡ್‌ನ್ ಮಿತ್ತ್ ಏರ್ಪದಾತ್ಂಡ್",
+       "botpasswords-updated-body": "ಬಾಟ್ ಪುದರ್ \"$1\"ದ ಬಾಟ್ ಸಂಕೇತಪದ {{GENDER:$2|ಬಳಕೆದಾರೆ}}\"$2\" ಕಾಲನವಿ ಆತ್ಂಡ್.",
        "botpasswords-deleted-title": "ಬಾಟ್ ಪಾಸ್‍ವರ್ಡ್‌ ದೆತ್ತ್‌ದಾತ್ಂಡ್",
+       "botpasswords-deleted-body": "ಬಾಟ್ ಪುದರ್ \"$1\"ದ ಬಾಟ್ ಸಂಕೇತಪದ {{GENDER:$2|ಬಳಕೆದಾರೆ}}\"$2\" ಮಾಜಾದುಂಡ್.",
+       "botpasswords-newpassword": "\n <strong>$1</strong> ದೊಟ್ಟುಗು ಉಳಗಮನೊಗು ಪೊಸ ಸಂಕೇತಪದ  <strong>$2</strong>. <em> ದಯಮಲ್ತ್ ಉಂದೆನ್ ಭವಿಷ್ಯತ್'ದ ಉಲ್ಲೇಖೊಗಾದ್ ದಾಖಲಿಸಾಲೆ</em> <br> (  ಇರೆನ ಉಳಗಮನ ಪುದರ್ ಸಂಭಾವ್ಯ ಬಳಕೆದಾರೆ ಪುದರ್ ಸಮಾನ ಅದುಪ್ಪುನ ಪರಾತ್ ಬಾಟ್'ಲೆಗ್, ಈರ್ <strong>$3</strong> ಬಳಕೆದಾರೆ ಪುದರಾದ್   ಬೊಕ <strong>$4</strong> ಸಂಕೇತಪದವಾದ್ ಗಳಸೊಲಿ)",
+       "botpasswords-no-provider": "BotPasswordsSessionProvider ತಿಕ್ಕುಜಿ.",
+       "botpasswords-restriction-failed": "ಬಾಟ್ ಸಂಕೇತಪದ ಮಿತಿಲು ಈ ಉಳಗಮನ ತಡೆಪುಂಡು.",
+       "botpasswords-invalid-name": "ವಿಸೂಚಿತ ಬಳಕೆದಾರೆಪುದರುಡು ಬಾಟ್ ಸಂಕೇತಪದ ಪ್ರತ್ಯೇಕಿಕ $1 ಇಜ್ಜಿ.",
+       "botpasswords-not-exist": "ಬಳಕೆದಾರೆ $1 ರೆಗ್ $2 ಪುದರುದ  ಒಂಜಿ ಬಾಟ್ ಸಂಕೇತ ಪದ ಇಜ್ಜಿ.",
+       "botpasswords-needs-reset": "ಬಾಟ್ ಪುದರ್ \"$1\"ದ ಬಾಟ್ ಸಂಕೇತಪದ {{GENDER:$2|ಬಳಕೆದಾರೆ}}\"$2\" ತಿರುಜೋಡನೆ ಆವೊಡು.",
+       "botpasswords-locked": "ಇರೆನ ಖಾತೆಗ್ ಬೀಗ ಪಾಡಿನ ಕಾರಣ ಈರ್ ಬಾಟ್ ಸಂಕೇತಪದೊಟು ಉಳಗಮನ ಮಲ್ಪರಾಪುಜಿ.",
+       "resetpass_forbidden": "ಸಂಕೇತಪದೊಲು ಬದಲಿಪರೆ ಆಪುಜಿ.",
+       "resetpass_forbidden-reason": "ಸಂಕೇತಪದೊಲು ಬದಲಾವರೆ ಆಪುಜಿ:$1",
+       "resetpass-no-info": "ಈ ಪುಟತ ನೇರ ಪ್ರವೇಶೊಗು ಈರ್ ಉಳಗಮನ ಆದಿಪ್ಪೊಡು.",
        "resetpass-submit-loggedin": "ಪ್ರವೇಶಪದೊನ್ ಬದಲಾವಣೆ ಮಲ್ಪುಲೆ",
        "resetpass-submit-cancel": "ವಜಾ ಮಲ್ಪುಲೆ",
+       "resetpass-wrong-oldpass": "ಅಮಾನ್ಯ ತಾತ್ಕಾಲಿಕ ಇಜಿಂಡ ಚಾಲ್ತಿ ಸಂಕೇತಪದ.\nಈರ್ ಇರೆನ ಸಂಕೇತಪದೊನು ಅದಗನೆ ಬದಲ್ ಮಲ್ತೊಂದರ್ ಇಜಿಂಡ ಒಂಜಿ ಪೊಸ ತಾತ್ಕಾಲಿಕ ಸಂಕೇತಪದೊಕು ಕೋರಿಕೆ ಕೊರ್ತರ್.",
+       "resetpass-recycled": "ಈರ್  ಇತ್ತೆ ಚಾಲ್ತಿ ಉಪ್ಪುನ ಸಂಕೇತಪದೊ ಅತ್ತಾಂದಿನ ಬೇತೆ ಸಂಕೇತಪದೊಕು ಬದಲು ಮಲ್ಪುಲೆ.",
        "resetpass-temp-password": "ತಾತ್ಕಾಲಿಕ ಪ್ರವೇಶಪದ:",
        "passwordreset": "ಪ್ರವೇಸೊ ಪದೊನ್ ಪಿರ ಸ್ತಾಪನೆ ಮಲ್ಪುಲೆ",
        "passwordreset-username": "ಸದಸ್ಯೆರ್ನ ಪುದರ್:",
        "search-category": "(ವರ್ಗ $1)",
        "search-file-match": "ಫೈಲ್‍ಡಿತ್ತಿ ವಿಸಯೊಗು ಸರಿ ಒಂಬುಂಡು",
        "search-suggest": "ಇಂದೆನ್ ನಾಡೊಂದುಲ್ಲರೆ: $1",
-       "search-interwiki-caption": "ಬಳà²\97ದ à²\87ತರ à²¯à³\8bà²\9cನà³\86ಲು",
+       "search-interwiki-caption": "ಸà³\8bದರಿ à²¯à³\8bà²\9cನà³\86ಲà³\86ನ à²«à²²à²¿à²¤à²¾à²\82ಶà³\8aಲು",
        "search-interwiki-default": "$1 ಫಲಿತಾಂಶೊಲು:",
        "search-interwiki-more": "(ಮಸ್ತ್)",
        "search-interwiki-more-results": "ನನಾತ್",
        "timezoneregion-europe": "ಯುರೋಪ್",
        "timezoneregion-indian": "ಹಿಂದೂ ಮಹಾಸಾಗರೊ",
        "timezoneregion-pacific": "ಪೆಸಿಫಿಕ್ ಮಹಾಸಾಗರೊ",
-       "allowemail": "ಬà³\87ತà³\86 à²¸à²¦à²¸à³\8dಯà³\86ರà³\86ಡà³\8dದà³\8d à²\87-ಮà³\88ಲà³\8dâ\80\8dಲà³\86ನà³\8d à²¦à³\86ತà³\8aನà³\8dಲ",
+       "allowemail": "à²\8eà²\82à²\95à³\8d à²\87-à²\85à²\82à²\9aà³\86 à²®à²²à³\8dಪರà³\86 à²¬à³\87ತà³\86 à²¸à²¦à²¸à³\8dಯà³\86ರà³\86à²\97à³\8d à²\85ನà³\81ಮತಿ à²\95à³\8aರà³\81ಲà³\86.",
        "prefs-searchoptions": "ನಾಡ್‍ಲ",
        "prefs-namespaces": "ಪುದರ್‍ದ ವರ್ಗೊಲು",
        "default": "ಮೂಲೊಸ್ಥಿತಿ",
        "prefs-pageswatchlist": "ವೀಕ್ಷಿತ (ತೂಯಿನ) ಪುಟೊಲು",
        "prefs-tokenwatchlist": "ಟೊಕನ್",
        "userrights": "ಸದಸ್ಯೆರೆ ಹಕ್ಕುಲು",
-       "userrights-lookup-user": "ಬಳà²\95à³\86ದಾರà³\86ರà³\86 à²\97à³\81à²\82ಪà³\81ಲà³\86ನà³\8d à²¨à²¿à²°à³\8dವಹಿಸಲ",
+       "userrights-lookup-user": "à²\92ರಿ à²¬à²³à²\95à³\86ದಾರà³\86ನà³\8d à²\86ಯà³\8dà²\95à³\86 à²®à²²à³\8dಪà³\81ಲà³\86",
        "userrights-user-editname": "ಒಂಜಿ ಸದಸ್ಯ ಪುದರ್ ಬರೆಲೆ",
        "userrights-editusergroup": "{{GENDER:$1|ಸದಸ್ಯೆರ್ನ}} ಗುಂಪುನ್ ಸೆರ್ಸಲೇ",
        "userrights-viewusergroup": "{{GENDER:$1|ಸದಸ್ಯೆರ್ನ}} ಗುಂಪುನ್ ತೂಲೆ",
        "grouppage-bot": "{{ns:project}}:ಬಾಟ್ಸ್",
        "grouppage-sysop": "{{ns:project}}:ನಿರ್ವಾಹಕೆರ್",
        "right-read": "ಪುಟಕ್‍ಲೆನ್ ಓದುಲೆ",
-       "right-edit": "ಪà³\81à²\9fà³\8aನà³\8d à²¸à²\82ಪಾದನà³\86 à²®à²²à³\8dಪà³\81ಲೆ",
+       "right-edit": "ಪà³\81à²\9fà³\8aಲà³\86ನà³\8d à²¸à²\82ಪಾದಿಸಾಲೆ",
        "right-move": "ಪುಟೊನ್",
        "right-writeapi": "ಬರವು ಎ.ಪಿ.ಐ. ದ ಉಪಯೋಗೊ",
        "right-delete": "ಪುಟೊಕುಲೆನ್ ಮಾಜಾಲೆ",
        "action-delete": "ಈ ಪುಟೊನ್ ಮಾಜಾಲೆ",
        "action-deleterevision": "ಈ ಆವೃತ್ತಿನ್ ಮಾಜಾಲೆ",
        "action-browsearchive": "ಮಜಾಯಿನಾ ಪುಟೋನ್ ನಡ್ಲೆ",
-       "action-undelete": "à²\88 à²ªà³\81à²\9fà³\8aನà³\8d à²®à²¾à²\9cಾಯಿನà³\86ನà³\8d à²°à²¦à³\8dದà³\8d à²®à²¾à²¨à³\8dಪà³\81ಲà³\87",
+       "action-undelete": "ಪà³\81à²\9fà³\8aಲà³\86ನà³\8d à²®à²¾à²\9cಾವà²\82ದà³\86 à²¦à³\80ಲà³\86",
        "action-sendemail": "ಇ-ಅಂಚೆ ಕಡಪುಡುಲೆ",
        "nchanges": "$1 {{PLURAL:$1|ಬದಲಾವಣೆ|ಬದಲಾವಣೆಲು}}",
        "enhancedrc-history": "ಇತಿಹಾಸೊ",
        "recentchanges-submit": "ತೋಜಾಲೆ",
        "rcfilters-quickfilters": "ಅರಿತ್ನ ವಿಸಯೊನ್ ಒರಿಪಾಲೆ",
        "rcfilters-savedqueries-rename": "ಪೊಸ ಪುದರ್",
-       "rcfilters-savedqueries-remove": "ದà³\86ಪà³\8dಪà³\81ಲೆ",
+       "rcfilters-savedqueries-remove": "ಮಾà²\9cಾಲೆ",
        "rcfilters-savedqueries-new-name-label": "ಪುದರ್",
        "rcfilters-savedqueries-cancel-label": "ವಜಾ ಮಲ್ಪುಲೆ",
-       "rcfilters-filterlist-whatsthis": "à²\89à²\82ದà³\81 à²¦à²¾à²¦à²¾?",
+       "rcfilters-filterlist-whatsthis": "à²\89à²\82ದà³\81 à²\8eà²\82à²\9a à²¬à³\87ಲà³\86 à²®à²²à³\8dಪà³\81à²\82ಡà³\81?",
        "rcfilters-filter-user-experience-level-learner-label": "ಕಲ್ಪುನರ್",
        "rcnotefrom": "<strong>$3, $4</strong> ಡ್ದ್ ಆತಿನ {{PLURAL:$5|ಬದಲಾವಣೆ|ಬದಲಾವಣೆಲು}} ತಿರ್ತ್ ಉಂಡು (ಒಟ್ಟುಗು <strong>$1</strong>  ತೋಜೊಂದುಂಡು).",
        "rclistfrom": "$2, $3 ಡ್ದ್ ಸುರುವಾತಿನ ಪೊಸ ಬದಲಾವಣೆಲೆನ್ ತೊಜ್ಪಾವು",
        "linkstoimage-more": "ಈ ಕಡತೊಗು $1 ಡ್ದ್ ಜಾಸ್ತಿ {{PLURAL:$1|ಪುಟೊ|ಪುಟೊಕುಲು}} ಸಂಪರ್ಕ ಕೊರ್ಪುಂಡು.\nಈ ಕಡೊತೊಗು ಮಾತ್ರ ಸಂಪರ್ಕ ಕೊರ್ಪಿನ {{PLURAL:$1|ಸುರುತ ಪುಟೊನು|ಸುರುತ $1 ಪುಟೊಕ್ಲೆನ್}} ತಿರ್ತ್‌ದ ಪಟ್ಟಿಡ್ ತೋಜಾದ್‌ಂಡ್.\n[[Special:WhatLinksHere/$2|ಇಡೀ ಪಟ್ಟಿಲಾ]] ಉಂಡು.",
        "nolinkstoimage": "ಈ ಫೈಲ್‍ಗ್ ಸಂಪರ್ಕೊ ಉಪ್ಪುನ ವಾ ಪುಟೊಲಾ ಇಜ್ಜಿ.",
        "linkstoimage-redirect": "$1 (ಕಡತ ಪುನರ್ನಿರ್ದೇಶನೊ) $2",
-       "sharedupload": "ಈ ಫೈಲ್’ನ್ ಮಸ್ತ್ ಜನ ಪಟ್ಟ್’ದುಲ್ಲೆರ್ ಅಂಚೆನೆ ಉಂದು ಮಸ್ತ್ ಪ್ರೊಜೆಕ್ಟ್’ಲೆಡ್ ಉಪಯೋಗಿಸೊಲಿ",
+       "sharedupload": "$1 ದವು ಈ ಕಡತ   ಬೊಕ ಉಂದೆನ್ ಇತರ ಯೋಜನೆಲೆಡ್ ಉಪಯೋಗಿಸಾವೊಲಿ",
        "sharedupload-desc-here": "ಈ ಪುಟೊ $1ಡ್ದ್ ಬೈದ್ಂಡ್ ಬೊಕ್ಕ ಬೇತೆ ಯೋಜನೆಲೆಡ್ ಗಲಸೊಲಿ.\n[$2 ಕಡತ ವಿವರಣೆ ಪುಟ]ತ ಮಿತ್ತ್ ವಿವರಣೆನ್ ತಿರ್ತ ಸಾಲ್‍ಡ್ ತೋಜಾದ್ಂಡ್.",
        "filepage-nofile": "ಈ ಪುದರ್‌ಡ್ ಒವ್ಲಾ ಕಡತ ಇಜ್ಜಿ.",
        "shared-repo-from": "$1 ನೆತ್ತ್",
        "whatlinkshere-hideimages": "$1 ಕಡತ ಕೊಂಡಿಲು",
        "whatlinkshere-filters": "ಅರಿಪೆಲು",
        "whatlinkshere-submit": "ಪೋಲೆ",
-       "blockip": "à²\88 à²¸à²¦à²¸à³\8dಯà³\86ರà³\86ನà³\8d à²¬à³\8dಲಾà²\95à³\8d à²®à²²à³\8dಪà³\81ಲà³\86",
+       "blockip": "ತಡà³\86ಪà³\81ಲà³\86 {{GENDER:$1|ಬಳà²\95à³\86ದಾರà³\86}}",
        "ipbreason": "ಕಾರಣೊ:",
        "ipboptions": "2 ಗಂಟೆಲು:2 hours,1 ದಿನ:1 day,3 ದಿನೊಕುಲು:3 days,1 ವಾರ:1 week,2 ವಾರೊಲು:2 weeks,1 ತಿಂಗೊಲು:1 month,3 ತಿಂಗೊಲು:3 months,6 ತಿಂಗೊಲು:6 months,1 ವರ್ಸ:1 year,ಅನಿರ್ಧಿಷ್ಟ:infinite",
        "blocklist": "ತಡೆ ಆತಿನ ಸದಸ್ಯೆರ್",
-       "ipblocklist": "ತಡà³\86ಪತà³\8dತà³\8dâ\80\99ದಿನ à²\90.ಪಿ à²µà²¿à²³à²¾à²¸à³\8aಲà³\81 à²\85à²\82à²\9aà³\86ನà³\86 à²¬à²³à²\95à³\86ದ à²ªà³\81ದರà³\8dâ\80\99ಲà³\81",
+       "ipblocklist": "ತಡà³\86ತಿದಿನ à²¬à²³à²\95à³\86ದಾರà³\86ರà³\8d",
        "blocklist-target": "ಗುರಿ",
        "blocklist-reason": "ಕಾರಣೊ",
        "ipblocklist-submit": "ನಾಡ್‍ಲೆ",
index 4c8b660..32b11e2 100644 (file)
        "nstab-special": "หน้าพิเศษ",
        "nstab-project": "หน้าโครงการ",
        "nstab-image": "ไฟล์",
-       "nstab-mediawiki": "สาร",
+       "nstab-mediawiki": "à¸\82à¹\89อà¸\84วาม",
        "nstab-template": "แม่แบบ",
        "nstab-help": "หน้าคำอธิบาย",
        "nstab-category": "หมวดหมู่",
        "searchdisabled": "การค้นหา {{SITENAME}} ถูกปิดใช้งาน \nคุณสามารถค้นหาโดยทางกูเกิลในระหว่างนั้น\nโปรดทราบว่าดัชนีเนื้อหา {{SITENAME} อาจล้าสมัย",
        "search-error": "มีข้อผิดพลาดขณะค้นหา: $1",
        "search-warning": "มีคำเตือนขณะค้นหา: $1",
-       "preferences": "à¸\95ัà¹\89à¸\87à¸\84à¹\88าà¸\9cูà¹\89à¹\83à¸\8aà¹\89",
-       "mypreferences": "à¸\95ัà¹\89à¸\87à¸\84à¹\88าà¸\9cูà¹\89à¹\83à¸\8aà¹\89",
+       "preferences": "à¸\84à¹\88าà¸\81ำหà¸\99à¸\94",
+       "mypreferences": "à¸\84à¹\88าà¸\81ำหà¸\99à¸\94",
        "prefs-edits": "จำนวนการแก้ไข:",
        "prefsnologintext2": "โปรดเข้าสู่ระบบเพื่อเปลี่ยนแปลงการตั้งค่าของคุณ",
        "prefs-skin": "หน้าตา",
        "datedefault": "ไม่ตั้งค่า",
        "prefs-labs": "คุณสมบัติทดลอง",
        "prefs-user-pages": "หน้าผู้ใช้",
-       "prefs-personal": "à¹\82à¸\9eรไฟล์ผู้ใช้",
+       "prefs-personal": "à¹\82à¸\9bรไฟล์ผู้ใช้",
        "prefs-rc": "เปลี่ยนแปลงล่าสุด",
        "prefs-watchlist": "รายการเฝ้าดู",
        "prefs-editwatchlist": "แก้ไขรายการเฝ้าดู",
        "prefs-email": "ตัวเลือกอีเมล",
        "prefs-rendering": "การแสดงผล",
        "saveprefs": "บันทึก",
-       "restoreprefs": "à¸\84ืà¸\99à¸\81ารà¸\95ัà¹\89à¸\87à¸\84à¹\88าà¸\9bริยายทั้งหมด (ในทุกส่วน)",
+       "restoreprefs": "à¸\84ืà¸\99à¸\84à¹\88าà¸\81ารà¸\95ัà¹\89à¸\87à¸\84à¹\88าà¹\80ริà¹\88มà¸\95à¹\89à¸\99ทั้งหมด (ในทุกส่วน)",
        "prefs-editing": "การแก้ไข",
        "searchresultshead": "ค้นหา",
        "stub-threshold": "ความยาวของหน้าที่ใช้เป็นเส้นแบ่งในการระบุหน้าโครง เพื่อจะให้มีการจัดรูปแบบเฉพาะตัว สำหรับลิงก์ที่โยงมายังโครง ($1):",
        "timezoneregion-europe": "ทวีปยุโรป",
        "timezoneregion-indian": "มหาสมุทรอินเดีย",
        "timezoneregion-pacific": "มหาสมุทรแปซิฟิก",
-       "allowemail": "อà¸\99ุà¸\8dาà¸\95à¹\83หà¹\89à¸\9cูà¹\89à¹\83à¸\8aà¹\89อืà¹\88à¸\99อีà¹\80มลหา",
+       "allowemail": "อà¸\99ุà¸\8dาà¸\95à¹\83หà¹\89à¸\9cูà¹\89à¹\83à¸\8aà¹\89อืà¹\88à¸\99สà¹\88à¸\87อีà¹\80มลà¸\96ึà¸\87à¸\89ัà¸\99à¹\84à¸\94à¹\89",
        "email-allow-new-users-label": "อนุญาตอีเมลจากผู้ใช้ใหม่",
        "email-blacklist-label": "ห้ามผู้ใช้เหล่านี้มิให้อีเมลหา:",
        "prefs-searchoptions": "ค้นหา",
        "prefs-namespaces": "เนมสเปซ",
-       "default": "à¸\84à¹\88าà¸\9bริยาย",
+       "default": "à¸\84à¹\88าà¹\80ริà¹\88มà¸\95à¹\89à¸\99",
        "prefs-files": "ไฟล์",
        "prefs-custom-css": "สไตล์ชีตปรับแต่งเอง",
        "prefs-custom-json": "JSON กำหนดเอง",
        "wantedtemplates": "แม่แบบที่ต้องการ",
        "mostlinked": "หน้าที่มีการเชื่อมโยงหามากที่สุด",
        "mostlinkedcategories": "หมวดหมู่ที่มีการเชื่อมโยงหามากที่สุด",
-       "mostlinkedtemplates": "หà¸\99à¹\89าà¸\97ีà¹\88มีà¸\81ารà¹\80à¸\8aืà¹\88อมà¹\82ยà¸\87หามากที่สุด",
+       "mostlinkedtemplates": "หà¸\99à¹\89าà¸\97ีà¹\88à¸\96ูà¸\81à¹\83à¸\8aà¹\89à¹\80à¸\9bà¹\87à¸\99à¹\81มà¹\88à¹\81à¸\9aà¸\9aมากที่สุด",
        "mostcategories": "หน้าที่มีหมวดหมู่มากที่สุด",
        "mostimages": "ไฟล์ที่มีการโยงหามากที่สุด",
        "mostinterwikis": "หน้าที่มีลิงก์ข้ามโครงการมากที่สุด",
        "booksources-invalid-isbn": "รหัส ISBN ที่ให้ไว้ไม่ถูกต้อง กรุณาตรวจสอบจากต้นฉบับอีกครั้ง",
        "specialloguserlabel": "ผู้ดำเนินการ:",
        "speciallogtitlelabel": "เป้าหมาย (ชื่อเรื่องหรือ {{ns:user}}:ชื่อผู้ใช้ สำหรับผู้ใช้):",
-       "log": "à¸\9bูม",
+       "log": "รายà¸\81ารà¸\9aัà¸\99à¸\97ึà¸\81",
        "logeventslist-submit": "แสดง",
        "logeventslist-more-filters": "แสดงปูมเพิ่ม:",
        "logeventslist-patrol-log": "ปูมการตรวจสอบ",
        "emailfrom": "จาก:",
        "emailto": "ถึง:",
        "emailsubject": "เรื่อง:",
-       "emailmessage": "สาร:",
+       "emailmessage": "à¸\82à¹\89อà¸\84วาม:",
        "emailsend": "ส่ง",
        "emailccme": "ส่งอีเมลสำเนาข้อความของฉันหาฉัน",
        "emailccsubject": "คัดลอกสารของคุณไป $1: $2",
        "sp-contributions-newbies": "แสดงการเข้ามีส่วนร่วมของบัญชีใหม่เท่านั้น",
        "sp-contributions-newbies-sub": "สำหรับบัญชีใหม่",
        "sp-contributions-newbies-title": "การเข้ามีส่วนร่วมสำหรับบัญชีใหม่",
-       "sp-contributions-blocklog": "à¸\9bูมการบล็อก",
+       "sp-contributions-blocklog": "รายà¸\81ารà¸\9aัà¸\99à¸\97ึà¸\81การบล็อก",
        "sp-contributions-suppresslog": "ระงับการมีส่วนร่วมของผู้ใช้",
        "sp-contributions-deleted": "การมีส่วนร่วมของผู้ใช้ที่ถูกลบ",
        "sp-contributions-uploads": "อัปโหลด",
-       "sp-contributions-logs": "à¸\9bูม",
+       "sp-contributions-logs": "รายà¸\81ารà¸\9aัà¸\99à¸\97ึà¸\81",
        "sp-contributions-talk": "คุย",
        "sp-contributions-userrights": "การจัดการสิทธิผู้ใช้",
        "sp-contributions-blocked-notice": "ปัจจุบันผู้ใช้นี้ถูกบล็อก\nปูมการบล็อกล่าสุดแสดงด้านล่างนี้เพื่อการอ้างอิง:",
        "tooltip-pt-anonuserpage": "หน้าผู้ใช้ของเลขที่อยู่ไอพีที่คุณกำลังใช้แก้ไข",
        "tooltip-pt-mytalk": "หน้าคุย{{GENDER:|ของคุณ}}",
        "tooltip-pt-anontalk": "อภิปรายเกี่ยวกับการแก้ไขจากเลขที่อยู่ไอพีนี้",
-       "tooltip-pt-preferences": "à¸\81ารà¸\95ัà¹\89à¸\87à¸\84à¹\88า{{GENDER:|ของคุณ}}",
+       "tooltip-pt-preferences": "à¸\84à¹\88าà¸\81ำหà¸\99à¸\94{{GENDER:|ของคุณ}}",
        "tooltip-pt-watchlist": "รายการหน้าที่คุณกำลังเฝ้าดูการเปลี่ยนแปลง",
        "tooltip-pt-mycontris": "รายการการเข้ามีส่วนร่วมของ{{GENDER:|คุณ}}",
        "tooltip-pt-anoncontribs": "รายการการแก้ไขจากเลขที่อยู่ไอพีนี้",
        "feedback-bugornote": "หากคุณได้อธิบายปัญหาทางเทคนิคในรายละเอียดแล้ว โปรด[$1 รายงานจุดบกพร่อง]\nมิฉะนั้น คุณสามารถแบบอย่างง่ายด้านล่าง ความเห็นของคุณจะถูกเพิ่มเข้าสู่ \"[$3 $2]\" ร่วมกับชื่อผู้ใช้ของคุณ",
        "feedback-cancel": "ยกเลิก",
        "feedback-close": "เสร็จสิ้น",
-       "feedback-message": "สาร:",
+       "feedback-message": "à¸\82à¹\89อà¸\84วาม:",
        "feedback-subject": "เรื่อง:",
        "feedback-submit": "ตกลง",
        "feedback-thanks": "ขอบคุณ! ผลป้อนกลับของคุณถูกโพสต์ไปยังหน้า \"[$2 $1]\" แล้ว",
index b09bc7e..c46e25d 100644 (file)
@@ -36,7 +36,7 @@
        "tog-hideminor": "Соңгы үзгәртүләр исемлегендә кече үзгәртүләр яшерелсен",
        "tog-hidepatrolled": "Тикшерелгән үзгәртүләр яңа үзгәртүләр исемлегеннән яшерелсен",
        "tog-newpageshidepatrolled": "Тикшерелгән битләр яңа битләр исемлегеннән яшерелсен",
-       "tog-hidecategorization": "Битләрне төркемләшүне ябу",
+       "tog-hidecategorization": "Битләрне төркемләшү яшерелсен",
        "tog-extendwatchlist": "Соңгыларын гына түгел, ә барлык үзгәртүләрне эченә алган, киңәйтелгән күзәтү исемлеге",
        "tog-usenewrc": "Соңгы үзгәртүләрдә һәм күзәтү исемлегендә үзгәрешләрне төркемләргә",
        "tog-numberheadings": "Атамалар автомат рәвештә номерлансын",
@@ -67,7 +67,7 @@
        "tog-watchlistreloadautomatically": "Фильтр алмашкан очракта күзәтү исемлеген автоматик рәвештә яңартырга (JavaScript кирәк)",
        "tog-watchlisthideanons": "Аноним кулланучыларның үзгәртүләре күзәтү исемлегеннән яшерелсен",
        "tog-watchlisthidepatrolled": "Тикшерелгән үзгәртүләр күзәтү исемлегеннән яшерелсен",
-       "tog-watchlisthidecategorization": "Битләрне төркемләшүне ябу",
+       "tog-watchlisthidecategorization": "Битләрне төркемләшү яшерелсен",
        "tog-ccmeonemails": "Башка кулланучыларга җибәргән хатларымның копияләре миңа да җибәрелсен",
        "tog-diffonly": "Юрама чагыштыру астында бит эчтәлеге күрсәтелмәсен",
        "tog-showhiddencats": "Яшерен төркемнәр күрсәтелсен",
        "sp-contributions-search": "Кертемне эзләү",
        "sp-contributions-username": "Кулланучының IP адресы яки исеме:",
        "sp-contributions-toponly": "Соңгы юрамадагы үзгәртүләр генә күрсәтелсен",
-       "sp-contributions-newonly": "Битләр ясау үзгәртмәләрен генә күрсәтү",
-       "sp-contributions-hideminor": "Кече үзгәртүләрне яшерергә",
+       "sp-contributions-newonly": "Битләр ясау үзгәртмәләре генә күрсәтелсен",
+       "sp-contributions-hideminor": "Кече үзгәртүләр яшерелсен",
        "sp-contributions-submit": "Эзләү",
        "whatlinkshere": "Бирегә нәрсә сылтый",
        "whatlinkshere-title": "$1 битенә сылтый торган битләр",
index 274aa37..b0c6cbb 100644 (file)
        "ipb_expiry_old": "Час закінчення — в минулому.",
        "ipb_expiry_temp": "Блокування із приховуванням імені користувача мають бути безстроковими.",
        "ipb_hide_invalid": "Неможливо приховати обліковий запис; з нього зроблено понад{{PLURAL:$1|одне редагування|$1 редагування|$1 редагувань}}.",
-       "ipb_already_blocked": "«$1» уже заблоковано. Для того, щоб призначити новий термін блокування, спочатку розблокуйте його.",
+       "ipb_already_blocked": "«$1» уже заблоковано.",
        "ipb-needreblock": "Користувач $1 вже заблокований. Хочете змінити параметри блокування?",
        "ipb-otherblocks-header": "{{PLURAL:$1|1=Інше блокування|Інші блокування}}",
        "unblock-hideuser": "Ви не можете розблокувати цього користувача, оскільки його ім'я було приховано.",
index 8d6f5ec..d204bcd 100644 (file)
        "edit-already-exists": "Li novele pâdje n' a savou esse ahivêye, ca cisse pâdje la egzistêye dedja.",
        "editwarning-warning": "Cwiter cisse pådje ci vos frè piede tos les candjmints ki vs avoz fwait.\nSi vs estoz elodj{{GENDER:|î|eye}}, vos ploz dismete cist adviertixhmint ci dins l' linwete « {{int:prefs-editing}} » di vos preferinces.",
        "content-model-wikitext": "wikitecse",
-       "duplicate-args-category": "Pådje eployant des dobes parametes dins les uzaedjes di modele",
+       "duplicate-args-category": "Pådjes eployant des dobes parametes dins les uzaedjes di modele",
        "post-expand-template-inclusion-warning": "'''Asteme:''' I gn a trop di modeles dins cisse pådje ci.\nSacwants di zels ni seront nén eployîs.",
        "post-expand-template-inclusion-category": "Pådjes ki l' inclusion d' modeles est foû limite",
        "viewpagelogs": "Vey les djournås po cisse pådje ci",
index c5ff8bc..2e677db 100644 (file)
        "session_fail_preview_html": "對唔住!有關嘅程序資料已經遺失,我哋唔能夠處理你嘅編輯。\n\n<em>由於{{SITENAME}}已經開放咗原 HTML 碼,預覽已經隱藏落嚟以預防 JavaScript 嘅攻擊。</em>\n<strong>如果呢個係正當嘅編輯嘗試,請再試過。</strong>\n如果重係唔得嘅話,請先[[Special:UserLogout|簽退]]後再簽到。",
        "token_suffix_mismatch": "'''因為你嘅用戶端度嘅編輯幣整壞咗一啲標點符號字元,你嘅編輯已經拒絕咗。'''個編輯已經拒絕,以防止嗰版嘅文字損毀。\n當你響度用緊一啲好多臭蟲,以網絡為主嘅匿名代理服務。",
        "edit_form_incomplete": "'''編輯表格嘅某個部份同server唔夾,請檢查多次你嘅編輯同埋再試多次。'''",
-       "editing": "而家喺度編輯$1",
+       "editing": "編輯緊$1",
        "creating": "開版$1",
-       "editingsection": "而家喺度編輯$1 (小節)",
-       "editingcomment": "而家喺度編輯$1 (新小節)",
+       "editingsection": "編輯緊$1 (小節)",
+       "editingcomment": "編輯緊$1 (新小節)",
        "editconflict": "由於編輯衝突,{{GENDER:|你|妳|你}}喺$1嘅修改記唔低。{{GENDER:|你|妳|你}}想唔想人手解決呢個衝突?",
        "explainconflict": "有其他人喺你開始編輯之後已經更改呢一頁。\n喺上面嗰個空間而家現存嘅頁面文字。\n你嘅更改會喺下面嘅文字空間顯示。\n你需要合併你嘅更改到原有嘅文字。\n喺你撳「$1」之後,'''只有'''喺上面嘅文字區會被儲存。",
        "yourtext": "你嘅文字",
        "sharedupload-desc-there": "呢個檔案係出自$1,可以喺其他計劃中使用。\n更多資訊請睇[$2 檔案描述頁]。",
        "sharedupload-desc-here": "呢個檔案係出自$1,可以喺其他計劃中使用。\n佢響嗰邊嘅[$2 檔案描述頁]響下面度顯示。",
        "sharedupload-desc-edit": "呢個文件喺$1同其他姊妹計劃度用咗,\n你可能喺佢嘅[$2文件描述頁]度改個描述先至得。",
-       "sharedupload-desc-create": "呢個文件喺$1同其他姊妹計劃度用咗,\n你可能喺佢嘅[$2文件描述頁]度改個描述先至得。",
+       "sharedupload-desc-create": "呢個文件來自$1,喺第啲姊妹計劃可能都有用到。\n你可以去改佢嘅[$2 檔案描述頁]。",
        "filepage-nofile": "冇同名嘅檔案存在。",
        "filepage-nofile-link": "冇同名嘅檔案存在,但係你可以[$1 上載佢]。",
        "uploadnewversion-linktext": "上載呢個檔案嘅一個新版本",
index 6095c3c..b2611c1 100644 (file)
        "grant-createaccount": "建立帳號",
        "grant-createeditmovepage": "建立、編輯與移動頁面",
        "grant-delete": "刪除頁面、修訂與日誌記錄",
-       "grant-editinterface": "編輯 MediaWiki 命名空間與站台範圍/使用者 JSON",
+       "grant-editinterface": "編輯 MediaWiki 命名空間與全站範圍/使用者 JSON",
        "grant-editmycssjs": "編輯您的使用者 CSS/JSON/JavaScript",
        "grant-editmyoptions": "編輯您的使用者偏好設定以及 JSON 設置",
        "grant-editmywatchlist": "編輯您的監視清單",
-       "grant-editsiteconfig": "編輯站台範圍與使用者 CSS/JS",
+       "grant-editsiteconfig": "編輯全站範圍與使用者 CSS/JS",
        "grant-editpage": "編輯現有的頁面",
        "grant-editprotected": "編輯受保護的頁面",
        "grant-highvolume": "大量編輯",
        "ipb-disableusertalk": "編輯自己的對話頁面",
        "ipb-change-block": "使用現有設定重新封鎖使用者",
        "ipb-confirm": "確認封鎖",
-       "ipb-sitewide": "站台範圍",
+       "ipb-sitewide": "全站範圍",
        "ipb-partial": "部分",
        "ipb-pages-label": "頁面",
        "ipb-namespaces-label": "命名空間",
        "emailblock": "停用電子郵件",
        "blocklist-nousertalk": "無法編輯自己的對話頁面",
        "blocklist-editing": "編輯",
-       "blocklist-editing-sitewide": "編輯(站台範圍)",
+       "blocklist-editing-sitewide": "編輯(全站範圍)",
        "blocklist-editing-page": "頁面",
        "blocklist-editing-ns": "命名空間",
        "ipblocklist-empty": "封鎖清單為空。",
        "ipb_expiry_old": "到期時間已過。",
        "ipb_expiry_temp": "隱藏使用者名稱的封鎖不可設定期限。",
        "ipb_hide_invalid": "無法禁止顯示此帳號;它擁有超過 $1 次的編輯。",
+       "ipb_hide_partial": "隱藏使用者封鎖必須是全站封鎖。",
        "ipb_already_blocked": "已經封鎖 \"$1\"。",
        "ipb-needreblock": "$1 已經被封鎖。您是否想變更設定?",
        "ipb-otherblocks-header": "其他{{PLURAL:$1|封鎖}}",
index 71d12ee..f3c2e12 100644 (file)
@@ -1348,11 +1348,10 @@ abstract class Maintenance {
         * @return IMaintainableDatabase
         */
        protected function getDB( $db, $groups = [], $wiki = false ) {
-               if ( is_null( $this->mDb ) ) {
+               if ( $this->mDb === null ) {
                        return wfGetDB( $db, $groups, $wiki );
-               } else {
-                       return $this->mDb;
                }
+               return $this->mDb;
        }
 
        /**
diff --git a/maintenance/archives/patch-drop-comment-fields.sql b/maintenance/archives/patch-drop-comment-fields.sql
new file mode 100644 (file)
index 0000000..a4b248d
--- /dev/null
@@ -0,0 +1,38 @@
+--
+-- patch-drop-comment-fields.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+ALTER TABLE /*_*/archive
+  DROP COLUMN ar_comment,
+  ALTER COLUMN ar_comment_id DROP DEFAULT;
+
+ALTER TABLE /*_*/ipblocks
+  DROP COLUMN ipb_reason,
+  ALTER COLUMN ipb_reason_id DROP DEFAULT;
+
+ALTER TABLE /*_*/image
+  DROP COLUMN img_description,
+  ALTER COLUMN img_description_id DROP DEFAULT;
+
+ALTER TABLE /*_*/oldimage
+  DROP COLUMN oi_description,
+  ALTER COLUMN oi_description_id DROP DEFAULT;
+
+ALTER TABLE /*_*/filearchive
+  DROP COLUMN fa_deleted_reason,
+  ALTER COLUMN fa_deleted_reason_id DROP DEFAULT,
+  DROP COLUMN fa_description,
+  ALTER COLUMN fa_description_id DROP DEFAULT;
+
+ALTER TABLE /*_*/recentchanges
+  DROP COLUMN rc_comment,
+  ALTER COLUMN rc_comment_id DROP DEFAULT;
+
+ALTER TABLE /*_*/logging
+  DROP COLUMN log_comment,
+  ALTER COLUMN log_comment_id DROP DEFAULT;
+
+ALTER TABLE /*_*/protected_titles
+  DROP COLUMN pt_reason,
+  ALTER COLUMN pt_reason_id DROP DEFAULT;
index cbab677..e681a04 100644 (file)
@@ -54,7 +54,7 @@ class BenchmarkPurge extends Benchmarker {
        }
 
        /**
-        * Run a bunch of URLs through SquidUpdate::purge()
+        * Run a bunch of URLs through CdnCacheUpdate::purge()
         * to benchmark Squid response times.
         * @param array $urls A bunch of URLs to purge
         * @param int $trials How many times to run the test?
index 9e35687..09e8211 100644 (file)
@@ -83,7 +83,6 @@ class DeleteBatch extends Maintenance {
                $dbw = $this->getDB( DB_MASTER );
 
                # Handle each entry
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $linenum = 1; !feof( $file ); $linenum++ ) {
                        $line = trim( fgets( $file ) );
                        if ( $line == '' ) {
index 747319d..b964417 100644 (file)
@@ -105,6 +105,7 @@ ERROR
        /**
         * Get the master DB handle for the current user batch. This is provided for the benefit
         * of authentication extensions which subclass this and work with wiki farms.
+        * @return IMaintainableDatabase
         */
        protected function getUserDB() {
                return $this->getDB( DB_MASTER );
diff --git a/maintenance/includes/MigrateActors.php b/maintenance/includes/MigrateActors.php
new file mode 100644 (file)
index 0000000..ceba9b5
--- /dev/null
@@ -0,0 +1,584 @@
+<?php
+/**
+ * Helper for migrating actors from pre-1.31 columns to the 'actor' table
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/../Maintenance.php';
+
+/**
+ * Maintenance script that migrates actors from pre-1.31 columns to the
+ * 'actor' table
+ *
+ * @ingroup Maintenance
+ */
+class MigrateActors extends LoggedUpdateMaintenance {
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Migrates actors from pre-1.31 columns to the \'actor\' table' );
+               $this->setBatchSize( 100 );
+       }
+
+       protected function getUpdateKey() {
+               return __CLASS__;
+       }
+
+       protected function doDBUpdates() {
+               global $wgActorTableSchemaMigrationStage;
+
+               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
+                       $this->output(
+                               "...cannot update while \$wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_WRITE_NEW\n"
+                       );
+                       return false;
+               }
+
+               $this->output( "Creating actor entries for all registered users\n" );
+               $end = 0;
+               $dbw = $this->getDB( DB_MASTER );
+               $max = $dbw->selectField( 'user', 'MAX(user_id)', '', __METHOD__ );
+               $count = 0;
+               while ( $end < $max ) {
+                       $start = $end + 1;
+                       $end = min( $start + $this->mBatchSize, $max );
+                       $this->output( "... $start - $end\n" );
+                       $dbw->insertSelect(
+                               'actor',
+                               'user',
+                               [ 'actor_user' => 'user_id', 'actor_name' => 'user_name' ],
+                               [ "user_id >= $start", "user_id <= $end" ],
+                               __METHOD__,
+                               [ 'IGNORE' ],
+                               [ 'ORDER BY' => [ 'user_id' ] ]
+                       );
+                       $count += $dbw->affectedRows();
+                       wfWaitForSlaves();
+               }
+               $this->output( "Completed actor creation, added $count new actor(s)\n" );
+
+               $errors = 0;
+               $errors += $this->migrateToTemp(
+                       'revision', 'rev_id', [ 'revactor_timestamp' => 'rev_timestamp', 'revactor_page' => 'rev_page' ],
+                       'rev_user', 'rev_user_text', 'revactor_rev', 'revactor_actor'
+               );
+               $errors += $this->migrate( 'archive', 'ar_id', 'ar_user', 'ar_user_text', 'ar_actor' );
+               $errors += $this->migrate( 'ipblocks', 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_by_actor' );
+               $errors += $this->migrate( 'image', 'img_name', 'img_user', 'img_user_text', 'img_actor' );
+               $errors += $this->migrate(
+                       'oldimage', [ 'oi_name', 'oi_timestamp' ], 'oi_user', 'oi_user_text', 'oi_actor'
+               );
+               $errors += $this->migrate( 'filearchive', 'fa_id', 'fa_user', 'fa_user_text', 'fa_actor' );
+               $errors += $this->migrate( 'recentchanges', 'rc_id', 'rc_user', 'rc_user_text', 'rc_actor' );
+               $errors += $this->migrate( 'logging', 'log_id', 'log_user', 'log_user_text', 'log_actor' );
+
+               $errors += $this->migrateLogSearch();
+
+               return $errors === 0;
+       }
+
+       /**
+        * Calculate a "next" condition and a display string
+        * @param IDatabase $dbw
+        * @param string[] $primaryKey Primary key of the table.
+        * @param object $row Database row
+        * @return array [ string $next, string $display ]
+        */
+       private function makeNextCond( $dbw, $primaryKey, $row ) {
+               $next = '';
+               $display = [];
+               for ( $i = count( $primaryKey ) - 1; $i >= 0; $i-- ) {
+                       $field = $primaryKey[$i];
+                       $display[] = $field . '=' . $row->$field;
+                       $value = $dbw->addQuotes( $row->$field );
+                       if ( $next === '' ) {
+                               $next = "$field > $value";
+                       } else {
+                               $next = "$field > $value OR $field = $value AND ($next)";
+                       }
+               }
+               $display = implode( ' ', array_reverse( $display ) );
+               return [ $next, $display ];
+       }
+
+       /**
+        * Make the subqueries for `actor_id`
+        * @param IDatabase $dbw
+        * @param string $userField User ID field name
+        * @param string $nameField User name field name
+        * @return string SQL fragment
+        */
+       private function makeActorIdSubquery( $dbw, $userField, $nameField ) {
+               $idSubquery = $dbw->buildSelectSubquery(
+                       'actor',
+                       'actor_id',
+                       [ "$userField = actor_user" ],
+                       __METHOD__
+               );
+               $nameSubquery = $dbw->buildSelectSubquery(
+                       'actor',
+                       'actor_id',
+                       [ "$nameField = actor_name" ],
+                       __METHOD__
+               );
+               return "CASE WHEN $userField = 0 OR $userField IS NULL THEN $nameSubquery ELSE $idSubquery END";
+       }
+
+       /**
+        * Add actors for anons in a set of rows
+        * @param IDatabase $dbw
+        * @param string $nameField
+        * @param object[] &$rows
+        * @param array &$complainedAboutUsers
+        * @param int &$countErrors
+        * @return int Count of actors inserted
+        */
+       private function addActorsForRows(
+               IDatabase $dbw, $nameField, array &$rows, array &$complainedAboutUsers, &$countErrors
+       ) {
+               $needActors = [];
+               $countActors = 0;
+
+               $keep = [];
+               foreach ( $rows as $index => $row ) {
+                       $keep[$index] = true;
+                       if ( $row->actor_id === null ) {
+                               // All registered users should have an actor_id already. So
+                               // if we have a usable name here, it means they didn't run
+                               // maintenance/cleanupUsersWithNoId.php
+                               $name = $row->$nameField;
+                               if ( User::isUsableName( $name ) ) {
+                                       if ( !isset( $complainedAboutUsers[$name] ) ) {
+                                               $complainedAboutUsers[$name] = true;
+                                               $this->error(
+                                                       "User name \"$name\" is usable, cannot create an anonymous actor for it."
+                                                       . " Run maintenance/cleanupUsersWithNoId.php to fix this situation.\n"
+                                               );
+                                       }
+                                       unset( $keep[$index] );
+                                       $countErrors++;
+                               } else {
+                                       $needActors[$name] = 0;
+                               }
+                       }
+               }
+               $rows = array_intersect_key( $rows, $keep );
+
+               if ( $needActors ) {
+                       $dbw->insert(
+                               'actor',
+                               array_map( function ( $v ) {
+                                       return [
+                                               'actor_name' => $v,
+                                       ];
+                               }, array_keys( $needActors ) ),
+                               __METHOD__
+                       );
+                       $countActors += $dbw->affectedRows();
+
+                       $res = $dbw->select(
+                               'actor',
+                               [ 'actor_id', 'actor_name' ],
+                               [ 'actor_name' => array_keys( $needActors ) ],
+                               __METHOD__
+                       );
+                       foreach ( $res as $row ) {
+                               $needActors[$row->actor_name] = $row->actor_id;
+                       }
+                       foreach ( $rows as $row ) {
+                               if ( $row->actor_id === null ) {
+                                       $row->actor_id = $needActors[$row->$nameField];
+                               }
+                       }
+               }
+
+               return $countActors;
+       }
+
+       /**
+        * Migrate actors in a table.
+        *
+        * Assumes any row with the actor field non-zero have already been migrated.
+        * Blanks the name field when migrating.
+        *
+        * @param string $table Table to migrate
+        * @param string|string[] $primaryKey Primary key of the table.
+        * @param string $userField User ID field name
+        * @param string $nameField User name field name
+        * @param string $actorField Actor field name
+        * @return int Number of errors
+        */
+       protected function migrate( $table, $primaryKey, $userField, $nameField, $actorField ) {
+               $complainedAboutUsers = [];
+
+               $primaryKey = (array)$primaryKey;
+               $pkFilter = array_flip( $primaryKey );
+               $this->output(
+                       "Beginning migration of $table.$userField and $table.$nameField to $table.$actorField\n"
+               );
+               wfWaitForSlaves();
+
+               $dbw = $this->getDB( DB_MASTER );
+               $actorIdSubquery = $this->makeActorIdSubquery( $dbw, $userField, $nameField );
+               $next = '1=1';
+               $countUpdated = 0;
+               $countActors = 0;
+               $countErrors = 0;
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               $table,
+                               array_merge( $primaryKey, [ $userField, $nameField, 'actor_id' => $actorIdSubquery ] ),
+                               [
+                                       $actorField => 0,
+                                       $next,
+                               ],
+                               __METHOD__,
+                               [
+                                       'ORDER BY' => $primaryKey,
+                                       'LIMIT' => $this->mBatchSize,
+                               ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Insert new actors for rows that need one
+                       $rows = iterator_to_array( $res );
+                       $lastRow = end( $rows );
+                       $countActors += $this->addActorsForRows(
+                               $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors
+                       );
+
+                       // Update the existing rows
+                       foreach ( $rows as $row ) {
+                               if ( !$row->actor_id ) {
+                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
+                                       $this->error(
+                                               "Could not make actor for row with $display "
+                                               . "$userField={$row->$userField} $nameField={$row->$nameField}\n"
+                                       );
+                                       $countErrors++;
+                                       continue;
+                               }
+                               $dbw->update(
+                                       $table,
+                                       [
+                                               $actorField => $row->actor_id,
+                                       ],
+                                       array_intersect_key( (array)$row, $pkFilter ) + [
+                                               $actorField => 0
+                                       ],
+                                       __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
+                       . "$countErrors error(s)\n"
+               );
+               return $countErrors;
+       }
+
+       /**
+        * Migrate actors in a table to a temporary table.
+        *
+        * Assumes the new table is named "{$table}_actor_temp", and it has two
+        * columns, in order, being the primary key of the original table and the
+        * actor ID field.
+        * Blanks the name field when migrating.
+        *
+        * @param string $table Table to migrate
+        * @param string $primaryKey Primary key of the table.
+        * @param array $extra Extra fields to copy
+        * @param string $userField User ID field name
+        * @param string $nameField User name field name
+        * @param string $newPrimaryKey Primary key of the new table.
+        * @param string $actorField Actor field name
+        */
+       protected function migrateToTemp(
+               $table, $primaryKey, $extra, $userField, $nameField, $newPrimaryKey, $actorField
+       ) {
+               $complainedAboutUsers = [];
+
+               $newTable = $table . '_actor_temp';
+               $this->output(
+                       "Beginning migration of $table.$userField and $table.$nameField to $newTable.$actorField\n"
+               );
+               wfWaitForSlaves();
+
+               $dbw = $this->getDB( DB_MASTER );
+               $actorIdSubquery = $this->makeActorIdSubquery( $dbw, $userField, $nameField );
+               $next = [];
+               $countUpdated = 0;
+               $countActors = 0;
+               $countErrors = 0;
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [ $table, $newTable ],
+                               [ $primaryKey, $userField, $nameField, 'actor_id' => $actorIdSubquery ] + $extra,
+                               [ $newPrimaryKey => null ] + $next,
+                               __METHOD__,
+                               [
+                                       'ORDER BY' => $primaryKey,
+                                       'LIMIT' => $this->mBatchSize,
+                               ],
+                               [
+                                       $newTable => [ 'LEFT JOIN', "{$primaryKey}={$newPrimaryKey}" ],
+                               ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Insert new actors for rows that need one
+                       $rows = iterator_to_array( $res );
+                       $lastRow = end( $rows );
+                       $countActors += $this->addActorsForRows(
+                               $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors
+                       );
+
+                       // Update rows
+                       if ( $rows ) {
+                               $inserts = [];
+                               $updates = [];
+                               foreach ( $rows as $row ) {
+                                       if ( !$row->actor_id ) {
+                                               list( , $display ) = $this->makeNextCond( $dbw, [ $primaryKey ], $row );
+                                               $this->error(
+                                                       "Could not make actor for row with $display "
+                                                       . "$userField={$row->$userField} $nameField={$row->$nameField}\n"
+                                               );
+                                               $countErrors++;
+                                               continue;
+                                       }
+                                       $ins = [
+                                               $newPrimaryKey => $row->$primaryKey,
+                                               $actorField => $row->actor_id,
+                                       ];
+                                       foreach ( $extra as $to => $from ) {
+                                               $ins[$to] = $row->$to; // It's aliased
+                                       }
+                                       $inserts[] = $ins;
+                                       $updates[] = $row->$primaryKey;
+                               }
+                               $this->beginTransaction( $dbw, __METHOD__ );
+                               $dbw->insert( $newTable, $inserts, __METHOD__ );
+                               $countUpdated += $dbw->affectedRows();
+                               $this->commitTransaction( $dbw, __METHOD__ );
+                       }
+
+                       // Calculate the "next" condition
+                       list( $n, $display ) = $this->makeNextCond( $dbw, [ $primaryKey ], $lastRow );
+                       $next = [ $n ];
+                       $this->output( "... $display\n" );
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
+                       . "$countErrors error(s)\n"
+               );
+               return $countErrors;
+       }
+
+       /**
+        * Migrate actors in the log_search table.
+        * @return int Number of errors
+        */
+       protected function migrateLogSearch() {
+               $complainedAboutUsers = [];
+
+               $primaryKey = [ 'ls_field', 'ls_value' ];
+               $pkFilter = array_flip( $primaryKey );
+               $this->output( "Beginning migration of log_search\n" );
+               wfWaitForSlaves();
+
+               $dbw = $this->getDB( DB_MASTER );
+               $countUpdated = 0;
+               $countActors = 0;
+               $countErrors = 0;
+
+               $next = '1=1';
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [
+                                       'ls' => $dbw->buildSelectSubquery(
+                                               'log_search',
+                                               'ls_value',
+                                               [
+                                                       'ls_field' => 'target_author_id',
+                                                       $next
+                                               ],
+                                               __METHOD__,
+                                               [
+                                                       'DISTINCT',
+                                                       'ORDER BY' => [ 'ls_value' ],
+                                                       'LIMIT' => $this->mBatchSize,
+                                               ]
+                                       ),
+                                       'actor'
+                               ],
+                               [
+                                       'ls_field' => $dbw->addQuotes( 'target_author_id' ),
+                                       'ls_value',
+                                       'actor_id'
+                               ],
+                               [],
+                               __METHOD__,
+                               [],
+                               [ 'actor' => [ 'LEFT JOIN', 'ls_value = ' . $dbw->buildStringCast( 'actor_user' ) ] ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Update the rows
+                       $del = [];
+                       foreach ( $res as $row ) {
+                               $lastRow = $row;
+                               if ( !$row->actor_id ) {
+                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
+                                       $this->error( "No actor for row with $display\n" );
+                                       $countErrors++;
+                                       continue;
+                               }
+                               $dbw->update(
+                                       'log_search',
+                                       [
+                                               'ls_field' => 'target_author_actor',
+                                               'ls_value' => $row->actor_id,
+                                       ],
+                                       [
+                                               'ls_field' => $row->ls_field,
+                                               'ls_value' => $row->ls_value,
+                                       ],
+                                       __METHOD__,
+                                       [ 'IGNORE' ]
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                               $del[] = $row->ls_value;
+                       }
+                       if ( $del ) {
+                               $dbw->delete(
+                                       'log_search', [ 'ls_field' => 'target_author_id', 'ls_value' => $del ], __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $next = '1=1';
+               while ( true ) {
+                       // Fetch the rows needing update
+                       $res = $dbw->select(
+                               [
+                                       'ls' => $dbw->buildSelectSubquery(
+                                               'log_search',
+                                               'ls_value',
+                                               [
+                                                       'ls_field' => 'target_author_ip',
+                                                       $next
+                                               ],
+                                               __METHOD__,
+                                               [
+                                                       'DISTINCT',
+                                                       'ORDER BY' => [ 'ls_value' ],
+                                                       'LIMIT' => $this->mBatchSize,
+                                               ]
+                                       ),
+                                       'actor'
+                               ],
+                               [
+                                       'ls_field' => $dbw->addQuotes( 'target_author_ip' ),
+                                       'ls_value',
+                                       'actor_id'
+                               ],
+                               [],
+                               __METHOD__,
+                               [],
+                               [ 'actor' => [ 'LEFT JOIN', 'ls_value = actor_name' ] ]
+                       );
+                       if ( !$res->numRows() ) {
+                               break;
+                       }
+
+                       // Insert new actors for rows that need one
+                       $rows = iterator_to_array( $res );
+                       $lastRow = end( $rows );
+                       $countActors += $this->addActorsForRows(
+                               $dbw, 'ls_value', $rows, $complainedAboutUsers, $countErrors
+                       );
+
+                       // Update the rows
+                       $del = [];
+                       foreach ( $rows as $row ) {
+                               if ( !$row->actor_id ) {
+                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
+                                       $this->error( "Could not make actor for row with $display\n" );
+                                       $countErrors++;
+                                       continue;
+                               }
+                               $dbw->update(
+                                       'log_search',
+                                       [
+                                               'ls_field' => 'target_author_actor',
+                                               'ls_value' => $row->actor_id,
+                                       ],
+                                       [
+                                               'ls_field' => $row->ls_field,
+                                               'ls_value' => $row->ls_value,
+                                       ],
+                                       __METHOD__,
+                                       [ 'IGNORE' ]
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                               $del[] = $row->ls_value;
+                       }
+                       if ( $del ) {
+                               $dbw->delete(
+                                       'log_search', [ 'ls_field' => 'target_author_ip', 'ls_value' => $del ], __METHOD__
+                               );
+                               $countUpdated += $dbw->affectedRows();
+                       }
+
+                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
+                       $this->output( "... $display\n" );
+                       wfWaitForSlaves();
+               }
+
+               $this->output(
+                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
+                       . "$countErrors error(s)\n"
+               );
+               return $countErrors;
+       }
+}
index f5a1e44..bc15b57 100644 (file)
  * @ingroup Maintenance
  */
 
-use Wikimedia\Rdbms\IDatabase;
+require_once __DIR__ . '/includes/MigrateActors.php';
 
-require_once __DIR__ . '/Maintenance.php';
-
-/**
- * Maintenance script that migrates actors from pre-1.31 columns to the
- * 'actor' table
- *
- * @ingroup Maintenance
- */
-class MigrateActors extends LoggedUpdateMaintenance {
-       public function __construct() {
-               parent::__construct();
-               $this->addDescription( 'Migrates actors from pre-1.31 columns to the \'actor\' table' );
-               $this->setBatchSize( 100 );
-       }
-
-       protected function getUpdateKey() {
-               return __CLASS__;
-       }
-
-       protected function doDBUpdates() {
-               global $wgActorTableSchemaMigrationStage;
-
-               if ( !( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
-                       $this->output(
-                               "...cannot update while \$wgActorTableSchemaMigrationStage lacks SCHEMA_COMPAT_WRITE_NEW\n"
-                       );
-                       return false;
-               }
-
-               $this->output( "Creating actor entries for all registered users\n" );
-               $end = 0;
-               $dbw = $this->getDB( DB_MASTER );
-               $max = $dbw->selectField( 'user', 'MAX(user_id)', '', __METHOD__ );
-               $count = 0;
-               while ( $end < $max ) {
-                       $start = $end + 1;
-                       $end = min( $start + $this->mBatchSize, $max );
-                       $this->output( "... $start - $end\n" );
-                       $dbw->insertSelect(
-                               'actor',
-                               'user',
-                               [ 'actor_user' => 'user_id', 'actor_name' => 'user_name' ],
-                               [ "user_id >= $start", "user_id <= $end" ],
-                               __METHOD__,
-                               [ 'IGNORE' ],
-                               [ 'ORDER BY' => [ 'user_id' ] ]
-                       );
-                       $count += $dbw->affectedRows();
-                       wfWaitForSlaves();
-               }
-               $this->output( "Completed actor creation, added $count new actor(s)\n" );
-
-               $errors = 0;
-               $errors += $this->migrateToTemp(
-                       'revision', 'rev_id', [ 'revactor_timestamp' => 'rev_timestamp', 'revactor_page' => 'rev_page' ],
-                       'rev_user', 'rev_user_text', 'revactor_rev', 'revactor_actor'
-               );
-               $errors += $this->migrate( 'archive', 'ar_id', 'ar_user', 'ar_user_text', 'ar_actor' );
-               $errors += $this->migrate( 'ipblocks', 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_by_actor' );
-               $errors += $this->migrate( 'image', 'img_name', 'img_user', 'img_user_text', 'img_actor' );
-               $errors += $this->migrate(
-                       'oldimage', [ 'oi_name', 'oi_timestamp' ], 'oi_user', 'oi_user_text', 'oi_actor'
-               );
-               $errors += $this->migrate( 'filearchive', 'fa_id', 'fa_user', 'fa_user_text', 'fa_actor' );
-               $errors += $this->migrate( 'recentchanges', 'rc_id', 'rc_user', 'rc_user_text', 'rc_actor' );
-               $errors += $this->migrate( 'logging', 'log_id', 'log_user', 'log_user_text', 'log_actor' );
-
-               $errors += $this->migrateLogSearch();
-
-               return $errors === 0;
-       }
-
-       /**
-        * Calculate a "next" condition and a display string
-        * @param IDatabase $dbw
-        * @param string[] $primaryKey Primary key of the table.
-        * @param object $row Database row
-        * @return array [ string $next, string $display ]
-        */
-       private function makeNextCond( $dbw, $primaryKey, $row ) {
-               $next = '';
-               $display = [];
-               for ( $i = count( $primaryKey ) - 1; $i >= 0; $i-- ) {
-                       $field = $primaryKey[$i];
-                       $display[] = $field . '=' . $row->$field;
-                       $value = $dbw->addQuotes( $row->$field );
-                       if ( $next === '' ) {
-                               $next = "$field > $value";
-                       } else {
-                               $next = "$field > $value OR $field = $value AND ($next)";
-                       }
-               }
-               $display = implode( ' ', array_reverse( $display ) );
-               return [ $next, $display ];
-       }
-
-       /**
-        * Make the subqueries for `actor_id`
-        * @param IDatabase $dbw
-        * @param string $userField User ID field name
-        * @param string $nameField User name field name
-        * @return string SQL fragment
-        */
-       private function makeActorIdSubquery( $dbw, $userField, $nameField ) {
-               $idSubquery = $dbw->buildSelectSubquery(
-                       'actor',
-                       'actor_id',
-                       [ "$userField = actor_user" ],
-                       __METHOD__
-               );
-               $nameSubquery = $dbw->buildSelectSubquery(
-                       'actor',
-                       'actor_id',
-                       [ "$nameField = actor_name" ],
-                       __METHOD__
-               );
-               return "CASE WHEN $userField = 0 OR $userField IS NULL THEN $nameSubquery ELSE $idSubquery END";
-       }
-
-       /**
-        * Add actors for anons in a set of rows
-        * @param IDatabase $dbw
-        * @param string $nameField
-        * @param object[] &$rows
-        * @param array &$complainedAboutUsers
-        * @param int &$countErrors
-        * @return int Count of actors inserted
-        */
-       private function addActorsForRows(
-               IDatabase $dbw, $nameField, array &$rows, array &$complainedAboutUsers, &$countErrors
-       ) {
-               $needActors = [];
-               $countActors = 0;
-
-               $keep = [];
-               foreach ( $rows as $index => $row ) {
-                       $keep[$index] = true;
-                       if ( $row->actor_id === null ) {
-                               // All registered users should have an actor_id already. So
-                               // if we have a usable name here, it means they didn't run
-                               // maintenance/cleanupUsersWithNoId.php
-                               $name = $row->$nameField;
-                               if ( User::isUsableName( $name ) ) {
-                                       if ( !isset( $complainedAboutUsers[$name] ) ) {
-                                               $complainedAboutUsers[$name] = true;
-                                               $this->error(
-                                                       "User name \"$name\" is usable, cannot create an anonymous actor for it."
-                                                       . " Run maintenance/cleanupUsersWithNoId.php to fix this situation.\n"
-                                               );
-                                       }
-                                       unset( $keep[$index] );
-                                       $countErrors++;
-                               } else {
-                                       $needActors[$name] = 0;
-                               }
-                       }
-               }
-               $rows = array_intersect_key( $rows, $keep );
-
-               if ( $needActors ) {
-                       $dbw->insert(
-                               'actor',
-                               array_map( function ( $v ) {
-                                       return [
-                                               'actor_name' => $v,
-                                       ];
-                               }, array_keys( $needActors ) ),
-                               __METHOD__
-                       );
-                       $countActors += $dbw->affectedRows();
-
-                       $res = $dbw->select(
-                               'actor',
-                               [ 'actor_id', 'actor_name' ],
-                               [ 'actor_name' => array_keys( $needActors ) ],
-                               __METHOD__
-                       );
-                       foreach ( $res as $row ) {
-                               $needActors[$row->actor_name] = $row->actor_id;
-                       }
-                       foreach ( $rows as $row ) {
-                               if ( $row->actor_id === null ) {
-                                       $row->actor_id = $needActors[$row->$nameField];
-                               }
-                       }
-               }
-
-               return $countActors;
-       }
-
-       /**
-        * Migrate actors in a table.
-        *
-        * Assumes any row with the actor field non-zero have already been migrated.
-        * Blanks the name field when migrating.
-        *
-        * @param string $table Table to migrate
-        * @param string|string[] $primaryKey Primary key of the table.
-        * @param string $userField User ID field name
-        * @param string $nameField User name field name
-        * @param string $actorField Actor field name
-        * @return int Number of errors
-        */
-       protected function migrate( $table, $primaryKey, $userField, $nameField, $actorField ) {
-               $complainedAboutUsers = [];
-
-               $primaryKey = (array)$primaryKey;
-               $pkFilter = array_flip( $primaryKey );
-               $this->output(
-                       "Beginning migration of $table.$userField and $table.$nameField to $table.$actorField\n"
-               );
-               wfWaitForSlaves();
-
-               $dbw = $this->getDB( DB_MASTER );
-               $actorIdSubquery = $this->makeActorIdSubquery( $dbw, $userField, $nameField );
-               $next = '1=1';
-               $countUpdated = 0;
-               $countActors = 0;
-               $countErrors = 0;
-               while ( true ) {
-                       // Fetch the rows needing update
-                       $res = $dbw->select(
-                               $table,
-                               array_merge( $primaryKey, [ $userField, $nameField, 'actor_id' => $actorIdSubquery ] ),
-                               [
-                                       $actorField => 0,
-                                       $next,
-                               ],
-                               __METHOD__,
-                               [
-                                       'ORDER BY' => $primaryKey,
-                                       'LIMIT' => $this->mBatchSize,
-                               ]
-                       );
-                       if ( !$res->numRows() ) {
-                               break;
-                       }
-
-                       // Insert new actors for rows that need one
-                       $rows = iterator_to_array( $res );
-                       $lastRow = end( $rows );
-                       $countActors += $this->addActorsForRows(
-                               $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors
-                       );
-
-                       // Update the existing rows
-                       foreach ( $rows as $row ) {
-                               if ( !$row->actor_id ) {
-                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
-                                       $this->error(
-                                               "Could not make actor for row with $display "
-                                               . "$userField={$row->$userField} $nameField={$row->$nameField}\n"
-                                       );
-                                       $countErrors++;
-                                       continue;
-                               }
-                               $dbw->update(
-                                       $table,
-                                       [
-                                               $actorField => $row->actor_id,
-                                       ],
-                                       array_intersect_key( (array)$row, $pkFilter ) + [
-                                               $actorField => 0
-                                       ],
-                                       __METHOD__
-                               );
-                               $countUpdated += $dbw->affectedRows();
-                       }
-
-                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
-                       $this->output( "... $display\n" );
-                       wfWaitForSlaves();
-               }
-
-               $this->output(
-                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
-                       . "$countErrors error(s)\n"
-               );
-               return $countErrors;
-       }
-
-       /**
-        * Migrate actors in a table to a temporary table.
-        *
-        * Assumes the new table is named "{$table}_actor_temp", and it has two
-        * columns, in order, being the primary key of the original table and the
-        * actor ID field.
-        * Blanks the name field when migrating.
-        *
-        * @param string $table Table to migrate
-        * @param string $primaryKey Primary key of the table.
-        * @param array $extra Extra fields to copy
-        * @param string $userField User ID field name
-        * @param string $nameField User name field name
-        * @param string $newPrimaryKey Primary key of the new table.
-        * @param string $actorField Actor field name
-        */
-       protected function migrateToTemp(
-               $table, $primaryKey, $extra, $userField, $nameField, $newPrimaryKey, $actorField
-       ) {
-               $complainedAboutUsers = [];
-
-               $newTable = $table . '_actor_temp';
-               $this->output(
-                       "Beginning migration of $table.$userField and $table.$nameField to $newTable.$actorField\n"
-               );
-               wfWaitForSlaves();
-
-               $dbw = $this->getDB( DB_MASTER );
-               $actorIdSubquery = $this->makeActorIdSubquery( $dbw, $userField, $nameField );
-               $next = [];
-               $countUpdated = 0;
-               $countActors = 0;
-               $countErrors = 0;
-               while ( true ) {
-                       // Fetch the rows needing update
-                       $res = $dbw->select(
-                               [ $table, $newTable ],
-                               [ $primaryKey, $userField, $nameField, 'actor_id' => $actorIdSubquery ] + $extra,
-                               [ $newPrimaryKey => null ] + $next,
-                               __METHOD__,
-                               [
-                                       'ORDER BY' => $primaryKey,
-                                       'LIMIT' => $this->mBatchSize,
-                               ],
-                               [
-                                       $newTable => [ 'LEFT JOIN', "{$primaryKey}={$newPrimaryKey}" ],
-                               ]
-                       );
-                       if ( !$res->numRows() ) {
-                               break;
-                       }
-
-                       // Insert new actors for rows that need one
-                       $rows = iterator_to_array( $res );
-                       $lastRow = end( $rows );
-                       $countActors += $this->addActorsForRows(
-                               $dbw, $nameField, $rows, $complainedAboutUsers, $countErrors
-                       );
-
-                       // Update rows
-                       if ( $rows ) {
-                               $inserts = [];
-                               $updates = [];
-                               foreach ( $rows as $row ) {
-                                       if ( !$row->actor_id ) {
-                                               list( , $display ) = $this->makeNextCond( $dbw, [ $primaryKey ], $row );
-                                               $this->error(
-                                                       "Could not make actor for row with $display "
-                                                       . "$userField={$row->$userField} $nameField={$row->$nameField}\n"
-                                               );
-                                               $countErrors++;
-                                               continue;
-                                       }
-                                       $ins = [
-                                               $newPrimaryKey => $row->$primaryKey,
-                                               $actorField => $row->actor_id,
-                                       ];
-                                       foreach ( $extra as $to => $from ) {
-                                               $ins[$to] = $row->$to; // It's aliased
-                                       }
-                                       $inserts[] = $ins;
-                                       $updates[] = $row->$primaryKey;
-                               }
-                               $this->beginTransaction( $dbw, __METHOD__ );
-                               $dbw->insert( $newTable, $inserts, __METHOD__ );
-                               $countUpdated += $dbw->affectedRows();
-                               $this->commitTransaction( $dbw, __METHOD__ );
-                       }
-
-                       // Calculate the "next" condition
-                       list( $n, $display ) = $this->makeNextCond( $dbw, [ $primaryKey ], $lastRow );
-                       $next = [ $n ];
-                       $this->output( "... $display\n" );
-               }
-
-               $this->output(
-                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
-                       . "$countErrors error(s)\n"
-               );
-               return $countErrors;
-       }
-
-       /**
-        * Migrate actors in the log_search table.
-        * @return int Number of errors
-        */
-       protected function migrateLogSearch() {
-               $complainedAboutUsers = [];
-
-               $primaryKey = [ 'ls_field', 'ls_value' ];
-               $pkFilter = array_flip( $primaryKey );
-               $this->output( "Beginning migration of log_search\n" );
-               wfWaitForSlaves();
-
-               $dbw = $this->getDB( DB_MASTER );
-               $countUpdated = 0;
-               $countActors = 0;
-               $countErrors = 0;
-
-               $next = '1=1';
-               while ( true ) {
-                       // Fetch the rows needing update
-                       $res = $dbw->select(
-                               [
-                                       'ls' => $dbw->buildSelectSubquery(
-                                               'log_search',
-                                               'ls_value',
-                                               [
-                                                       'ls_field' => 'target_author_id',
-                                                       $next
-                                               ],
-                                               __METHOD__,
-                                               [
-                                                       'DISTINCT',
-                                                       'ORDER BY' => [ 'ls_value' ],
-                                                       'LIMIT' => $this->mBatchSize,
-                                               ]
-                                       ),
-                                       'actor'
-                               ],
-                               [
-                                       'ls_field' => $dbw->addQuotes( 'target_author_id' ),
-                                       'ls_value',
-                                       'actor_id'
-                               ],
-                               [],
-                               __METHOD__,
-                               [],
-                               [ 'actor' => [ 'LEFT JOIN', 'ls_value = ' . $dbw->buildStringCast( 'actor_user' ) ] ]
-                       );
-                       if ( !$res->numRows() ) {
-                               break;
-                       }
-
-                       // Update the rows
-                       $del = [];
-                       foreach ( $res as $row ) {
-                               $lastRow = $row;
-                               if ( !$row->actor_id ) {
-                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
-                                       $this->error( "No actor for row with $display\n" );
-                                       $countErrors++;
-                                       continue;
-                               }
-                               $dbw->update(
-                                       'log_search',
-                                       [
-                                               'ls_field' => 'target_author_actor',
-                                               'ls_value' => $row->actor_id,
-                                       ],
-                                       [
-                                               'ls_field' => $row->ls_field,
-                                               'ls_value' => $row->ls_value,
-                                       ],
-                                       __METHOD__,
-                                       [ 'IGNORE' ]
-                               );
-                               $countUpdated += $dbw->affectedRows();
-                               $del[] = $row->ls_value;
-                       }
-                       if ( $del ) {
-                               $dbw->delete(
-                                       'log_search', [ 'ls_field' => 'target_author_id', 'ls_value' => $del ], __METHOD__
-                               );
-                               $countUpdated += $dbw->affectedRows();
-                       }
-
-                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
-                       $this->output( "... $display\n" );
-                       wfWaitForSlaves();
-               }
-
-               $next = '1=1';
-               while ( true ) {
-                       // Fetch the rows needing update
-                       $res = $dbw->select(
-                               [
-                                       'ls' => $dbw->buildSelectSubquery(
-                                               'log_search',
-                                               'ls_value',
-                                               [
-                                                       'ls_field' => 'target_author_ip',
-                                                       $next
-                                               ],
-                                               __METHOD__,
-                                               [
-                                                       'DISTINCT',
-                                                       'ORDER BY' => [ 'ls_value' ],
-                                                       'LIMIT' => $this->mBatchSize,
-                                               ]
-                                       ),
-                                       'actor'
-                               ],
-                               [
-                                       'ls_field' => $dbw->addQuotes( 'target_author_ip' ),
-                                       'ls_value',
-                                       'actor_id'
-                               ],
-                               [],
-                               __METHOD__,
-                               [],
-                               [ 'actor' => [ 'LEFT JOIN', 'ls_value = actor_name' ] ]
-                       );
-                       if ( !$res->numRows() ) {
-                               break;
-                       }
-
-                       // Insert new actors for rows that need one
-                       $rows = iterator_to_array( $res );
-                       $lastRow = end( $rows );
-                       $countActors += $this->addActorsForRows(
-                               $dbw, 'ls_value', $rows, $complainedAboutUsers, $countErrors
-                       );
-
-                       // Update the rows
-                       $del = [];
-                       foreach ( $rows as $row ) {
-                               if ( !$row->actor_id ) {
-                                       list( , $display ) = $this->makeNextCond( $dbw, $primaryKey, $row );
-                                       $this->error( "Could not make actor for row with $display\n" );
-                                       $countErrors++;
-                                       continue;
-                               }
-                               $dbw->update(
-                                       'log_search',
-                                       [
-                                               'ls_field' => 'target_author_actor',
-                                               'ls_value' => $row->actor_id,
-                                       ],
-                                       [
-                                               'ls_field' => $row->ls_field,
-                                               'ls_value' => $row->ls_value,
-                                       ],
-                                       __METHOD__,
-                                       [ 'IGNORE' ]
-                               );
-                               $countUpdated += $dbw->affectedRows();
-                               $del[] = $row->ls_value;
-                       }
-                       if ( $del ) {
-                               $dbw->delete(
-                                       'log_search', [ 'ls_field' => 'target_author_ip', 'ls_value' => $del ], __METHOD__
-                               );
-                               $countUpdated += $dbw->affectedRows();
-                       }
-
-                       list( $next, $display ) = $this->makeNextCond( $dbw, $primaryKey, $lastRow );
-                       $this->output( "... $display\n" );
-                       wfWaitForSlaves();
-               }
-
-               $this->output(
-                       "Completed migration, updated $countUpdated row(s) with $countActors new actor(s), "
-                       . "$countErrors error(s)\n"
-               );
-               return $countErrors;
-       }
-}
-
-$maintClass = "MigrateActors";
+$maintClass = MigrateActors::class;
 require_once RUN_MAINTENANCE_IF_MAIN;
index 555f2d1..8025df1 100644 (file)
@@ -47,15 +47,6 @@ class MigrateComments extends LoggedUpdateMaintenance {
        }
 
        protected function doDBUpdates() {
-               global $wgCommentTableSchemaMigrationStage;
-
-               if ( $wgCommentTableSchemaMigrationStage < MIGRATION_WRITE_NEW ) {
-                       $this->output(
-                               "...cannot update while \$wgCommentTableSchemaMigrationStage < MIGRATION_WRITE_NEW\n"
-                       );
-                       return false;
-               }
-
                $this->migrateToTemp(
                        'revision', 'rev_id', 'rev_comment', 'revcomment_rev', 'revcomment_comment_id'
                );
@@ -138,13 +129,18 @@ class MigrateComments extends LoggedUpdateMaintenance {
         * @param string $oldField Old comment field name
         */
        protected function migrate( $table, $primaryKey, $oldField ) {
+               $dbw = $this->getDB( DB_MASTER );
+               if ( !$dbw->fieldExists( $table, $oldField, __METHOD__ ) ) {
+                       $this->output( "No need to migrate $table.$oldField, field does not exist\n" );
+                       return;
+               }
+
                $newField = $oldField . '_id';
                $primaryKey = (array)$primaryKey;
                $pkFilter = array_flip( $primaryKey );
                $this->output( "Beginning migration of $table.$oldField to $table.$newField\n" );
                wfWaitForSlaves();
 
-               $dbw = $this->getDB( DB_MASTER );
                $next = '1=1';
                $countUpdated = 0;
                $countComments = 0;
@@ -229,6 +225,12 @@ class MigrateComments extends LoggedUpdateMaintenance {
         * @param string $newField New comment field name
         */
        protected function migrateToTemp( $table, $primaryKey, $oldField, $newPrimaryKey, $newField ) {
+               $dbw = $this->getDB( DB_MASTER );
+               if ( !$dbw->fieldExists( $table, $oldField, __METHOD__ ) ) {
+                       $this->output( "No need to migrate $table.$oldField, field does not exist\n" );
+                       return;
+               }
+
                $newTable = $table . '_comment_temp';
                $this->output( "Beginning migration of $table.$oldField to $newTable.$newField\n" );
                wfWaitForSlaves();
index 6d14f8a..9c20c67 100644 (file)
@@ -86,7 +86,6 @@ class MoveBatch extends Maintenance {
 
                # Setup complete, now start
                $dbw = $this->getDB( DB_MASTER );
-               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                for ( $linenum = 1; !feof( $file ); $linenum++ ) {
                        $line = fgets( $file );
                        if ( $line === false ) {
diff --git a/maintenance/mssql/archives/patch-drop-comment-fields.sql b/maintenance/mssql/archives/patch-drop-comment-fields.sql
new file mode 100644 (file)
index 0000000..bdc8c91
--- /dev/null
@@ -0,0 +1,55 @@
+--
+-- patch-drop-comment-fields.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+DECLARE @sql nvarchar(max),
+       @id sysname;
+
+ALTER TABLE /*_*/archive DROP CONSTRAINT DF_ar_comment, COLUMN ar_comment;
+ALTER TABLE /*_*/archive DROP CONSTRAINT DF_ar_comment_id;
+
+ALTER TABLE /*_*/ipblocks DROP CONSTRAINT DF_ipb_reason, COLUMN ipb_reason;
+ALTER TABLE /*_*/ipblocks DROP CONSTRAINT DF_ipb_reason_id;
+
+ALTER TABLE /*_*/image DROP CONSTRAINT DF_img_description, COLUMN img_description;
+ALTER TABLE /*_*/image DROP CONSTRAINT DF_img_description_id;
+
+ALTER TABLE /*_*/oldimage DROP CONSTRAINT DF_oi_description, COLUMN oi_description;
+ALTER TABLE /*_*/oldimage DROP CONSTRAINT DF_oi_description_id;
+
+ALTER TABLE /*_*/filearchive DROP CONSTRAINT DF_fa_deleted_reason, COLUMN fa_deleted_reason;
+ALTER TABLE /*_*/filearchive DROP CONSTRAINT DF_fa_deleted_reason_id;
+ALTER TABLE /*_*/filearchive DROP CONSTRAINT DF_fa_description, COLUMN fa_description;
+ALTER TABLE /*_*/filearchive DROP CONSTRAINT DF_fa_description_id;
+
+SET @sql = 'ALTER TABLE /*_*/recentchanges DROP CONSTRAINT ';
+SELECT @id = df.name
+FROM sys.default_constraints df
+JOIN sys.columns c
+       ON c.object_id = df.parent_object_id
+       AND c.column_id = df.parent_column_id
+WHERE
+       df.parent_object_id = OBJECT_ID('/*_*/recentchanges')
+       AND c.name = 'rc_comment';
+SET @sql = @sql + @id;
+EXEC sp_executesql @sql;
+ALTER TABLE /*_*/recentchanges DROP COLUMN rc_comment;
+ALTER TABLE /*_*/recentchanges DROP CONSTRAINT DF_rc_comment_id;
+
+SET @sql = 'ALTER TABLE /*_*/logging DROP CONSTRAINT ';
+SELECT @id = df.name
+FROM sys.default_constraints df
+JOIN sys.columns c
+       ON c.object_id = df.parent_object_id
+       AND c.column_id = df.parent_column_id
+WHERE
+       df.parent_object_id = OBJECT_ID('/*_*/logging')
+       AND c.name = 'log_comment';
+SET @sql = @sql + @id;
+EXEC sp_executesql @sql;
+ALTER TABLE /*_*/logging DROP COLUMN log_comment;
+ALTER TABLE /*_*/logging DROP CONSTRAINT DF_log_comment_id;
+
+ALTER TABLE /*_*/protected_titles DROP CONSTRAINT DF_pt_reason, COLUMN pt_reason;
+ALTER TABLE /*_*/protected_titles DROP CONSTRAINT DF_pt_reason_id;
index 4ecc6db..72ea69d 100644 (file)
@@ -270,8 +270,7 @@ CREATE TABLE /*_*/archive (
    ar_id int NOT NULL PRIMARY KEY IDENTITY,
    ar_namespace SMALLINT NOT NULL DEFAULT 0,
    ar_title NVARCHAR(255) NOT NULL DEFAULT '',
-   ar_comment NVARCHAR(255) NOT NULL CONSTRAINT DF_ar_comment DEFAULT '',
-   ar_comment_id bigint NOT NULL CONSTRAINT DF_ar_comment_id DEFAULT 0 CONSTRAINT FK_ar_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+   ar_comment_id bigint NOT NULL CONSTRAINT FK_ar_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
    ar_user INT CONSTRAINT ar_user__user_id__fk FOREIGN KEY REFERENCES /*_*/mwuser(user_id),
    ar_user_text NVARCHAR(255) NOT NULL CONSTRAINT DF_ar_user_text DEFAULT '',
    ar_actor bigint NOT NULL CONSTRAINT DF_ar_actor DEFAULT 0,
@@ -651,12 +650,8 @@ CREATE TABLE /*_*/ipblocks (
   -- User name of blocker
   ipb_by_text nvarchar(255) NOT NULL default '',
 
-  -- Text comment made by blocker.
-  ipb_reason nvarchar(255) NOT NULL CONSTRAINT DF_ipb_reason DEFAULT '',
-
   -- Key to comment_id. Text comment made by blocker.
-  -- ("DEFAULT 0" is temporary, signaling that ipb_reason should be used)
-  ipb_reason_id bigint NOT NULL CONSTRAINT DF_ipb_reason_id DEFAULT 0 CONSTRAINT FK_ipb_reason_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  ipb_reason_id bigint NOT NULL CONSTRAINT FK_ipb_reason_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
 
   -- Creation (or refresh) date in standard YMDHMS form.
   -- IP blocks expire automatically.
@@ -776,8 +771,7 @@ CREATE TABLE /*_*/image (
 
   -- Description field as entered by the uploader.
   -- This is displayed in image upload history and logs.
-  img_description nvarchar(255) NOT NULL CONSTRAINT DF_img_description DEFAULT '',
-  img_description_id bigint NOT NULL CONSTRAINT DF_img_description_id DEFAULT 0 CONSTRAINT FK_img_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  img_description_id bigint NOT NULL CONSTRAINT FK_img_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
 
   -- user_id and user_name of uploader.
   img_user int REFERENCES /*_*/mwuser(user_id) ON DELETE SET NULL,
@@ -825,8 +819,7 @@ CREATE TABLE /*_*/oldimage (
   oi_width int NOT NULL default 0,
   oi_height int NOT NULL default 0,
   oi_bits int NOT NULL default 0,
-  oi_description nvarchar(255) NOT NULL CONSTRAINT DF_oi_description DEFAULT '',
-  oi_description_id bigint NOT NULL CONSTRAINT DF_oi_description_id DEFAULT 0 CONSTRAINT FK_oi_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  oi_description_id bigint NOT NULL CONSTRAINT FK_oi_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
   oi_user int REFERENCES /*_*/mwuser(user_id),
   oi_user_text nvarchar(255) NOT NULL CONSTRAINT DF_oi_user_text DEFAULT '',
   oi_actor bigint NOT NULL CONSTRAINT DF_oi_actor DEFAULT 0,
@@ -877,8 +870,7 @@ CREATE TABLE /*_*/filearchive (
   -- Deletion information, if this file is deleted.
   fa_deleted_user int,
   fa_deleted_timestamp varchar(14) default '',
-  fa_deleted_reason nvarchar(max) CONSTRAINT DF_fa_deleted_reason DEFAULT '',
-  fa_deleted_reason_id bigint NOT NULL CONSTRAINT DF_fa_deleted_reason_id DEFAULT 0 CONSTRAINT FK_fa_deleted_reason_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  fa_deleted_reason_id bigint NOT NULL CONSTRAINT FK_fa_deleted_reason_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
 
   -- Duped fields from image
   fa_size int default 0,
@@ -889,8 +881,7 @@ CREATE TABLE /*_*/filearchive (
   fa_media_type varchar(16) default null,
   fa_major_mime varchar(16) not null default 'unknown',
   fa_minor_mime nvarchar(100) default 'unknown',
-  fa_description nvarchar(255) CONSTRAINT DF_fa_description DEFAULT '',
-  fa_description_id bigint NOT NULL CONSTRAINT DF_fa_description_id DEFAULT 0 CONSTRAINT FK_fa_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  fa_description_id bigint NOT NULL CONSTRAINT FK_fa_description_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
   fa_user int default 0 REFERENCES /*_*/mwuser(user_id) ON DELETE SET NULL,
   fa_user_text nvarchar(255) CONSTRAINT DF_fa_user_text DEFAULT '',
   fa_actor bigint NOT NULL CONSTRAINT DF_fa_actor DEFAULT 0,
@@ -995,8 +986,7 @@ CREATE TABLE /*_*/recentchanges (
   rc_title nvarchar(255) NOT NULL default '',
 
   -- as in revision...
-  rc_comment nvarchar(255) NOT NULL default '',
-  rc_comment_id bigint NOT NULL CONSTRAINT DF_rc_comment_id DEFAULT 0 CONSTRAINT FK_rc_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  rc_comment_id bigint NOT NULL CONSTRAINT FK_rc_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
   rc_minor bit NOT NULL default 0,
 
   -- Edits by user accounts with the 'bot' rights key are
@@ -1189,12 +1179,8 @@ CREATE TABLE /*_*/logging (
   log_title nvarchar(255) NOT NULL default '',
   log_page int NULL, -- NOT an FK, logging entries are inserted for deleted pages which still reference the deleted page ids
 
-  -- Freeform text. Interpreted as edit history comments.
-  log_comment nvarchar(255) NOT NULL default '',
-
   -- Key to comment_id. Comment summarizing the change.
-  -- ("DEFAULT 0" is temporary, signaling that log_comment should be used)
-  log_comment_id bigint NOT NULL CONSTRAINT DF_log_comment_id DEFAULT 0 CONSTRAINT FK_log_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  log_comment_id bigint NOT NULL CONSTRAINT FK_log_comment_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
 
   -- miscellaneous parameters:
   -- LF separated list (old system) or serialized PHP array (new system)
@@ -1358,8 +1344,7 @@ CREATE TABLE /*_*/protected_titles (
   pt_namespace int NOT NULL,
   pt_title nvarchar(255) NOT NULL,
   pt_user int REFERENCES /*_*/mwuser(user_id) ON DELETE SET NULL,
-  pt_reason nvarchar(255) CONSTRAINT DF_pt_reason DEFAULT '',
-  pt_reason_id bigint NOT NULL CONSTRAINT DF_pt_reason_id DEFAULT 0 CONSTRAINT FK_pt_reason_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
+  pt_reason_id bigint NOT NULL CONSTRAINT FK_pt_reason_id FOREIGN KEY REFERENCES /*_*/comment(comment_id),
   pt_timestamp varchar(14) NOT NULL,
   pt_expiry varchar(14) NOT NULL,
   pt_create_perm nvarchar(60) NOT NULL,
diff --git a/maintenance/oracle/archives/patch-drop-comment-fields.sql b/maintenance/oracle/archives/patch-drop-comment-fields.sql
new file mode 100644 (file)
index 0000000..ea3c641
--- /dev/null
@@ -0,0 +1,30 @@
+--
+-- patch-drop-comment-fields.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+ALTER TABLE &mw_prefix.archive DROP COLUMN ar_comment;
+ALTER TABLE &mw_prefix.archive MODIFY ar_comment_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.ipblocks DROP COLUMN ipb_reason;
+ALTER TABLE &mw_prefix.ipblocks MODIFY ipb_reason_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.image DROP COLUMN img_description;
+ALTER TABLE &mw_prefix.image MODIFY img_description_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.oldimage DROP COLUMN oi_description;
+ALTER TABLE &mw_prefix.oldimage MODIFY oi_description_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.filearchive DROP COLUMN fa_deleted_reason;
+ALTER TABLE &mw_prefix.filearchive MODIFY fa_deleted_reason_id DEFAULT NULL,
+ALTER TABLE &mw_prefix.filearchive DROP COLUMN fa_description;
+ALTER TABLE &mw_prefix.filearchive MODIFY fa_description_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.recentchanges DROP COLUMN rc_comment;
+ALTER TABLE &mw_prefix.recentchanges MODIFY rc_comment_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.logging DROP COLUMN log_comment;
+ALTER TABLE &mw_prefix.logging MODIFY log_comment_id DEFAULT NULL;
+
+ALTER TABLE &mw_prefix.protected_titles DROP COLUMN pt_reason;
+ALTER TABLE &mw_prefix.protected_titles MODIFY pt_reason_id DEFAULT NULL;
index 2c20fff..7a8160f 100644 (file)
@@ -248,8 +248,7 @@ CREATE TABLE &mw_prefix.archive (
   ar_id          NUMBER NOT NULL,
   ar_namespace   NUMBER    DEFAULT 0 NOT NULL,
   ar_title       VARCHAR2(255)         NOT NULL,
-  ar_comment     VARCHAR2(255),
-  ar_comment_id  NUMBER DEFAULT 0 NOT NULL,
+  ar_comment_id  NUMBER NOT NULL,
   ar_user        NUMBER          DEFAULT 0 NOT NULL,
   ar_user_text   VARCHAR2(255)         NULL,
   ar_actor       NUMBER          DEFAULT 0 NOT NULL,
@@ -497,8 +496,7 @@ CREATE TABLE &mw_prefix.ipblocks (
   ipb_by                NUMBER      DEFAULT 0 NOT NULL,
   ipb_by_text           VARCHAR2(255)      NULL,
   ipb_by_actor          NUMBER      DEFAULT 0 NOT NULL,
-  ipb_reason            VARCHAR2(255)      NULL,
-  ipb_reason_id         NUMBER DEFAULT 0 NOT NULL,
+  ipb_reason_id         NUMBER NOT NULL,
   ipb_timestamp         TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
   ipb_auto              CHAR(1)         DEFAULT '0' NOT NULL,
   ipb_anon_only         CHAR(1)         DEFAULT '0' NOT NULL,
@@ -549,8 +547,7 @@ CREATE TABLE &mw_prefix.image (
   img_media_type   VARCHAR2(32),
   img_major_mime   VARCHAR2(32) DEFAULT 'unknown',
   img_minor_mime   VARCHAR2(100) DEFAULT 'unknown',
-  img_description  VARCHAR2(255),
-  img_description_id  NUMBER DEFAULT 0 NOT NULL,
+  img_description_id  NUMBER NOT NULL,
   img_user         NUMBER       DEFAULT 0 NOT NULL,
   img_user_text    VARCHAR2(255)      NULL,
   img_actor        NUMBER       DEFAULT 0 NOT NULL,
@@ -574,8 +571,7 @@ CREATE TABLE &mw_prefix.oldimage (
   oi_width         NUMBER      DEFAULT 0 NOT NULL,
   oi_height        NUMBER      DEFAULT 0 NOT NULL,
   oi_bits          NUMBER      DEFAULT 0 NOT NULL,
-  oi_description   VARCHAR2(255),
-  oi_description_id  NUMBER DEFAULT 0 NOT NULL,
+  oi_description_id  NUMBER NOT NULL,
   oi_user          NUMBER          DEFAULT 0 NOT NULL,
   oi_user_text     VARCHAR2(255)         NULL,
   oi_actor         NUMBER          DEFAULT 0 NOT NULL,
@@ -606,8 +602,7 @@ CREATE TABLE &mw_prefix.filearchive (
   fa_storage_key        VARCHAR2(64),
   fa_deleted_user       NUMBER          DEFAULT 0 NOT NULL,
   fa_deleted_timestamp  TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
-  fa_deleted_reason     CLOB,
-  fa_deleted_reason_id  NUMBER DEFAULT 0 NOT NULL,
+  fa_deleted_reason_id  NUMBER NOT NULL,
   fa_size               NUMBER     DEFAULT 0 NOT NULL,
   fa_width              NUMBER     DEFAULT 0 NOT NULL,
   fa_height             NUMBER     DEFAULT 0 NOT NULL,
@@ -616,8 +611,7 @@ CREATE TABLE &mw_prefix.filearchive (
   fa_media_type         VARCHAR2(32) DEFAULT NULL,
   fa_major_mime         VARCHAR2(32) DEFAULT 'unknown',
   fa_minor_mime         VARCHAR2(100) DEFAULT 'unknown',
-  fa_description        VARCHAR2(255),
-  fa_description_id     NUMBER DEFAULT 0 NOT NULL,
+  fa_description_id     NUMBER NOT NULL,
   fa_user               NUMBER          DEFAULT 0 NOT NULL,
   fa_user_text          VARCHAR2(255)         NULL,
   fa_actor              NUMBER          DEFAULT 0 NOT NULL,
@@ -687,8 +681,7 @@ CREATE TABLE &mw_prefix.recentchanges (
   rc_actor           NUMBER          DEFAULT 0 NOT NULL,
   rc_namespace       NUMBER     DEFAULT 0 NOT NULL,
   rc_title           VARCHAR2(255)         NOT NULL,
-  rc_comment         VARCHAR2(255),
-  rc_comment_id      NUMBER DEFAULT 0 NOT NULL,
+  rc_comment_id      NUMBER NOT NULL,
   rc_minor           CHAR(1)         DEFAULT '0' NOT NULL,
   rc_bot             CHAR(1)         DEFAULT '0' NOT NULL,
   rc_new             CHAR(1)         DEFAULT '0' NOT NULL,
@@ -788,8 +781,7 @@ CREATE TABLE &mw_prefix.logging (
   log_namespace   NUMBER     DEFAULT 0 NOT NULL,
   log_title       VARCHAR2(255)         NOT NULL,
   log_page                             NUMBER,
-  log_comment     VARCHAR2(255),
-  log_comment_id  NUMBER DEFAULT 0 NOT NULL,
+  log_comment_id  NUMBER NOT NULL,
   log_params      CLOB,
   log_deleted     CHAR(1)      DEFAULT '0' NOT NULL
 );
@@ -906,8 +898,7 @@ CREATE TABLE &mw_prefix.protected_titles (
   pt_namespace   NUMBER           DEFAULT 0 NOT NULL,
   pt_title       VARCHAR2(255)    NOT NULL,
   pt_user        NUMBER                  NOT NULL,
-  pt_reason      VARCHAR2(255),
-  pt_reason_id   NUMBER DEFAULT 0 NOT NULL,
+  pt_reason_id   NUMBER NOT NULL,
   pt_timestamp   TIMESTAMP(6) WITH TIME ZONE  NOT NULL,
   pt_expiry      VARCHAR2(14) NOT NULL,
   pt_create_perm VARCHAR2(60) NOT NULL
index 6eb2d6d..ec2eff4 100644 (file)
@@ -198,7 +198,7 @@ class PopulateArchiveRevId extends LoggedUpdateMaintenance {
                $rev = null;
 
                $mainPage = Title::newMainPage();
-               $pageId = $mainPage ? $mainPage->getArticleId() : null;
+               $pageId = $mainPage ? $mainPage->getArticleID() : null;
                if ( $pageId ) {
                        $rev = $dbw->selectRow(
                                'revision',
index b6858c5..87fb396 100644 (file)
@@ -243,8 +243,7 @@ CREATE TABLE archive (
   ar_page_id        INTEGER          NULL,
   ar_parent_id      INTEGER          NULL,
   ar_sha1           TEXT         NOT NULL DEFAULT '',
-  ar_comment        TEXT         NOT NULL DEFAULT '',
-  ar_comment_id     INTEGER      NOT NULL DEFAULT 0,
+  ar_comment_id     INTEGER      NOT NULL,
   ar_user           INTEGER      NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   ar_user_text      TEXT         NOT NULL DEFAULT '',
   ar_actor          INTEGER      NOT NULL DEFAULT 0,
@@ -408,8 +407,7 @@ CREATE TABLE ipblocks (
   ipb_by                INTEGER      NOT NULL  DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
   ipb_by_text           TEXT         NOT NULL  DEFAULT '',
   ipb_by_actor          INTEGER      NOT NULL  DEFAULT 0,
-  ipb_reason            TEXT         NOT NULL  DEFAULT '',
-  ipb_reason_id         INTEGER      NOT NULL  DEFAULT 0,
+  ipb_reason_id         INTEGER      NOT NULL,
   ipb_timestamp         TIMESTAMPTZ  NOT NULL,
   ipb_auto              SMALLINT     NOT NULL  DEFAULT 0,
   ipb_anon_only         SMALLINT     NOT NULL  DEFAULT 0,
@@ -449,8 +447,7 @@ CREATE TABLE image (
   img_media_type   TEXT,
   img_major_mime   TEXT                DEFAULT 'unknown',
   img_minor_mime   TEXT                DEFAULT 'unknown',
-  img_description  TEXT      NOT NULL  DEFAULT '',
-  img_description_id INTEGER NOT NULL  DEFAULT 0,
+  img_description_id INTEGER NOT NULL,
   img_user         INTEGER   NOT NULL  DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   img_user_text    TEXT      NOT NULL  DEFAULT '',
   img_actor        INTEGER   NOT NULL  DEFAULT 0,
@@ -468,8 +465,7 @@ CREATE TABLE oldimage (
   oi_width         INTEGER      NOT NULL,
   oi_height        INTEGER      NOT NULL,
   oi_bits          SMALLINT         NULL,
-  oi_description   TEXT         NOT NULL DEFAULT '',
-  oi_description_id INTEGER     NOT NULL DEFAULT 0,
+  oi_description_id INTEGER     NOT NULL,
   oi_user          INTEGER      NOT NULL DEFAULT 0  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   oi_user_text     TEXT         NOT NULL DEFAULT '',
   oi_actor         INTEGER      NOT NULL DEFAULT 0,
@@ -496,8 +492,7 @@ CREATE TABLE filearchive (
   fa_storage_key        TEXT,
   fa_deleted_user       INTEGER          NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   fa_deleted_timestamp  TIMESTAMPTZ  NOT NULL,
-  fa_deleted_reason     TEXT         NOT NULL  DEFAULT '',
-  fa_deleted_reason_id  INTEGER      NOT NULL  DEFAULT 0,
+  fa_deleted_reason_id  INTEGER      NOT NULL,
   fa_size               INTEGER      NOT NULL,
   fa_width              INTEGER      NOT NULL,
   fa_height             INTEGER      NOT NULL,
@@ -506,8 +501,7 @@ CREATE TABLE filearchive (
   fa_media_type         TEXT,
   fa_major_mime         TEXT                   DEFAULT 'unknown',
   fa_minor_mime         TEXT                   DEFAULT 'unknown',
-  fa_description        TEXT         NOT NULL DEFAULT '',
-  fa_description_id     INTEGER      NOT NULL DEFAULT 0,
+  fa_description_id     INTEGER      NOT NULL,
   fa_user               INTEGER      NOT NULL DEFAULT 0 REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
   fa_user_text          TEXT         NOT NULL DEFAULT '',
   fa_actor              INTEGER      NOT NULL DEFAULT 0,
@@ -559,8 +553,7 @@ CREATE TABLE recentchanges (
   rc_actor           INTEGER      NOT NULL  DEFAULT 0,
   rc_namespace       SMALLINT     NOT NULL,
   rc_title           TEXT         NOT NULL,
-  rc_comment         TEXT         NOT NULL  DEFAULT '',
-  rc_comment_id      INTEGER      NOT NULL  DEFAULT 0,
+  rc_comment_id      INTEGER      NOT NULL,
   rc_minor           SMALLINT     NOT NULL  DEFAULT 0,
   rc_bot             SMALLINT     NOT NULL  DEFAULT 0,
   rc_new             SMALLINT     NOT NULL  DEFAULT 0,
@@ -657,8 +650,7 @@ CREATE TABLE logging (
   log_actor       INTEGER      NOT NULL DEFAULT 0,
   log_namespace   SMALLINT     NOT NULL,
   log_title       TEXT         NOT NULL,
-  log_comment     TEXT         NOT NULL DEFAULT '',
-  log_comment_id  INTEGER      NOT NULL DEFAULT 0,
+  log_comment_id  INTEGER      NOT NULL,
   log_params      TEXT,
   log_deleted     SMALLINT     NOT NULL DEFAULT 0,
   log_user_text   TEXT         NOT NULL DEFAULT '',
@@ -767,8 +759,7 @@ CREATE TABLE protected_titles (
   pt_namespace   SMALLINT    NOT NULL,
   pt_title       TEXT        NOT NULL,
   pt_user        INTEGER         NULL  REFERENCES mwuser(user_id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
-  pt_reason      TEXT        NOT NULL DEFAULT '',
-  pt_reason_id   INTEGER     NOT NULL DEFAULT 0,
+  pt_reason_id   INTEGER     NOT NULL,
   pt_timestamp   TIMESTAMPTZ NOT NULL,
   pt_expiry      TIMESTAMPTZ     NULL,
   pt_create_perm TEXT        NOT NULL DEFAULT '',
index 7760f1d..7004102 100644 (file)
@@ -69,8 +69,8 @@ jquery:
 
 jquery.client:
   type: tar
-  src: https://registry.npmjs.org/jquery-client/-/jquery-client-2.0.1.tgz
-  integrity: sha256-tizJojJ55YYdKh67Zj/ho/9IAkDDA2UGKpcNvzn96Zs=
+  src: https://registry.npmjs.org/jquery-client/-/jquery-client-2.0.2.tgz
+  integrity: sha256-8c8nBbBykHEMc4I7ksdKJvvw/P7WkaC2X46RTPdz/pw=
   dest:
     package/AUTHORS.txt:
     package/jquery.client.js:
@@ -110,6 +110,16 @@ oojs:
     package/LICENSE-MIT:
     package/README.md:
 
+oojs-router:
+  type: tar
+  src: https://registry.npmjs.org/oojs-router/-/oojs-router-0.2.0.tgz
+  integrity: sha384-VngYqdQ3vTDMXbm4e4FUZCCGos7fB0Jkr9V+kBL5MElprK1h0yQZOzBNnMHtSJS/
+  dest:
+    package/dist/oojs-router.js:
+    package/LICENSE:
+    package/AUTHORS.txt:
+    package/History.md:
+
 ooui:
   type: tar
   src: https://registry.npmjs.org/oojs-ui/-/oojs-ui-0.30.2.tgz
diff --git a/maintenance/sqlite/archives/patch-archive-drop-ar_comment.sql b/maintenance/sqlite/archives/patch-archive-drop-ar_comment.sql
new file mode 100644 (file)
index 0000000..b412ebb
--- /dev/null
@@ -0,0 +1,47 @@
+--
+-- patch-archive-drop-ar_comment.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/archive_tmp;
+CREATE TABLE /*_*/archive_tmp (
+  ar_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ar_namespace int NOT NULL default 0,
+  ar_title varchar(255) binary NOT NULL default '',
+  ar_comment_id bigint unsigned NOT NULL,
+  ar_user int unsigned NOT NULL default 0,
+  ar_user_text varchar(255) binary NOT NULL DEFAULT '',
+  ar_actor bigint unsigned NOT NULL DEFAULT 0,
+  ar_timestamp binary(14) NOT NULL default '',
+  ar_minor_edit tinyint NOT NULL default 0,
+  ar_rev_id int unsigned NOT NULL,
+  ar_text_id int unsigned NOT NULL DEFAULT 0,
+  ar_deleted tinyint unsigned NOT NULL default 0,
+  ar_len int unsigned,
+  ar_page_id int unsigned,
+  ar_parent_id int unsigned default NULL,
+  ar_sha1 varbinary(32) NOT NULL default '',
+  ar_content_model varbinary(32) DEFAULT NULL,
+  ar_content_format varbinary(64) DEFAULT NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/archive_tmp (
+       ar_id, ar_namespace, ar_title, ar_comment_id, ar_user, ar_user_text, ar_actor,
+       ar_timestamp, ar_minor_edit, ar_rev_id, ar_text_id, ar_deleted,
+       ar_len, ar_page_id, ar_parent_id, ar_sha1, ar_content_model, ar_content_format
+  ) SELECT
+       ar_id, ar_namespace, ar_title, ar_comment_id, ar_user, ar_user_text, ar_actor,
+       ar_timestamp, ar_minor_edit, ar_rev_id, ar_text_id, ar_deleted,
+       ar_len, ar_page_id, ar_parent_id, ar_sha1, ar_content_model, ar_content_format
+  FROM /*_*/archive;
+
+DROP TABLE /*_*/archive;
+ALTER TABLE /*_*/archive_tmp RENAME TO /*_*/archive;
+CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
+CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp);
+CREATE INDEX /*i*/ar_actor_timestamp ON /*_*/archive (ar_actor,ar_timestamp);
+CREATE UNIQUE INDEX /*i*/ar_revid_uniq ON /*_*/archive (ar_rev_id);
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-filearchive-drop-fa_description.sql b/maintenance/sqlite/archives/patch-filearchive-drop-fa_description.sql
new file mode 100644 (file)
index 0000000..5880031
--- /dev/null
@@ -0,0 +1,58 @@
+--
+-- patch-filearchive-drop-fa_description.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/filearchive_tmp;
+CREATE TABLE /*_*/filearchive_tmp (
+  fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  fa_name varchar(255) binary NOT NULL default '',
+  fa_archive_name varchar(255) binary default '',
+  fa_storage_group varbinary(16),
+  fa_storage_key varbinary(64) default '',
+  fa_deleted_user int,
+  fa_deleted_timestamp binary(14) default '',
+  fa_deleted_reason_id bigint unsigned NOT NULL,
+  fa_size int unsigned default 0,
+  fa_width int default 0,
+  fa_height int default 0,
+  fa_metadata mediumblob,
+  fa_bits int default 0,
+  fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+  fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
+  fa_minor_mime varbinary(100) default "unknown",
+  fa_description_id bigint unsigned NOT NULL,
+  fa_user int unsigned default 0,
+  fa_user_text varchar(255) binary DEFAULT '',
+  fa_actor bigint unsigned NOT NULL DEFAULT 0,
+  fa_timestamp binary(14) default '',
+  fa_deleted tinyint unsigned NOT NULL default 0,
+  fa_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/filearchive_tmp (
+       fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key,
+       fa_deleted_user, fa_deleted_timestamp, fa_deleted_reason_id,
+       fa_size, fa_width, fa_height, fa_metadata, fa_bits,
+       fa_media_type, fa_major_mime, fa_minor_mime, fa_description_id,
+       fa_user, fa_user_text, fa_actor, fa_timestamp, fa_deleted, fa_sha1
+  ) SELECT
+       fa_id, fa_name, fa_archive_name, fa_storage_group, fa_storage_key,
+       fa_deleted_user, fa_deleted_timestamp, fa_deleted_reason_id,
+       fa_size, fa_width, fa_height, fa_metadata, fa_bits,
+       fa_media_type, fa_major_mime, fa_minor_mime, fa_description_id,
+       fa_user, fa_user_text, fa_actor, fa_timestamp, fa_deleted, fa_sha1
+  FROM /*_*/filearchive;
+
+DROP TABLE /*_*/filearchive;
+ALTER TABLE /*_*/filearchive_tmp RENAME TO /*_*/filearchive;
+CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp);
+CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key);
+CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp);
+CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp);
+CREATE INDEX /*i*/fa_actor_timestamp ON /*_*/filearchive (fa_actor,fa_timestamp);
+CREATE INDEX /*i*/fa_sha1 ON /*_*/filearchive (fa_sha1(10));
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-image-drop-img_description.sql b/maintenance/sqlite/archives/patch-image-drop-img_description.sql
new file mode 100644 (file)
index 0000000..c7bb90c
--- /dev/null
@@ -0,0 +1,47 @@
+--
+-- patch-image-drop-img_description.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/image_tmp;
+CREATE TABLE /*_*/image_tmp (
+  img_name varchar(255) binary NOT NULL default '' PRIMARY KEY,
+  img_size int unsigned NOT NULL default 0,
+  img_width int NOT NULL default 0,
+  img_height int NOT NULL default 0,
+  img_metadata mediumblob NOT NULL,
+  img_bits int NOT NULL default 0,
+  img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+  img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  img_minor_mime varbinary(100) NOT NULL default "unknown",
+  img_description_id bigint unsigned NOT NULL,
+  img_user int unsigned NOT NULL default 0,
+  img_user_text varchar(255) binary NOT NULL DEFAULT '',
+  img_actor bigint unsigned NOT NULL DEFAULT 0,
+  img_timestamp varbinary(14) NOT NULL default '',
+  img_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/image_tmp (
+       img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description_id, img_user,
+       img_user_text, img_actor, img_timestamp, img_sha1
+  ) SELECT
+       img_name, img_size, img_width, img_height, img_metadata, img_bits,
+       img_media_type, img_major_mime, img_minor_mime, img_description_id, img_user,
+       img_user_text, img_actor, img_timestamp, img_sha1
+  FROM /*_*/image;
+
+DROP TABLE /*_*/image;
+ALTER TABLE /*_*/image_tmp RENAME TO /*_*/image;
+CREATE INDEX /*i*/img_user_timestamp ON /*_*/image (img_user,img_timestamp);
+CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp);
+CREATE INDEX /*i*/img_actor_timestamp ON /*_*/image (img_actor,img_timestamp);
+CREATE INDEX /*i*/img_size ON /*_*/image (img_size);
+CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
+CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
+CREATE INDEX /*i*/img_media_mime ON /*_*/image (img_media_type,img_major_mime,img_minor_mime);
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-ipblocks-drop-ipb_reason.sql b/maintenance/sqlite/archives/patch-ipblocks-drop-ipb_reason.sql
new file mode 100644 (file)
index 0000000..de5faa0
--- /dev/null
@@ -0,0 +1,53 @@
+--
+-- patch-ipblocks-drop-ipb_reason.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS ipblocks_tmp;
+CREATE TABLE /*_*/ipblocks_tmp (
+  ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  ipb_address tinyblob NOT NULL,
+  ipb_user int unsigned NOT NULL default 0,
+  ipb_by int unsigned NOT NULL default 0,
+  ipb_by_text varchar(255) binary NOT NULL default '',
+  ipb_by_actor bigint unsigned NOT NULL DEFAULT 0,
+  ipb_reason_id bigint unsigned NOT NULL,
+  ipb_timestamp binary(14) NOT NULL default '',
+  ipb_auto bool NOT NULL default 0,
+  ipb_anon_only bool NOT NULL default 0,
+  ipb_create_account bool NOT NULL default 1,
+  ipb_enable_autoblock bool NOT NULL default '1',
+  ipb_expiry varbinary(14) NOT NULL default '',
+  ipb_range_start tinyblob NOT NULL,
+  ipb_range_end tinyblob NOT NULL,
+  ipb_deleted bool NOT NULL default 0,
+  ipb_block_email bool NOT NULL default 0,
+  ipb_allow_usertalk bool NOT NULL default 0,
+  ipb_parent_block_id int default NULL,
+  ipb_sitewide bool NOT NULL default 1
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/ipblocks_tmp (
+       ipb_id, ipb_address, ipb_user, ipb_by, ipb_by_text, ipb_by_actor, ipb_reason_id,
+       ipb_timestamp, ipb_auto, ipb_anon_only, ipb_create_account,
+       ipb_enable_autoblock, ipb_expiry, ipb_range_start, ipb_range_end,
+       ipb_deleted, ipb_block_email, ipb_allow_usertalk, ipb_parent_block_id, ipb_sitewide
+  ) SELECT
+       ipb_id, ipb_address, ipb_user, ipb_by, ipb_by_text, ipb_by_actor, ipb_reason_id,
+       ipb_timestamp, ipb_auto, ipb_anon_only, ipb_create_account,
+       ipb_enable_autoblock, ipb_expiry, ipb_range_start, ipb_range_end,
+       ipb_deleted, ipb_block_email, ipb_allow_usertalk, ipb_parent_block_id, ipb_sitewide
+  FROM /*_*/ipblocks;
+
+DROP TABLE /*_*/ipblocks;
+ALTER TABLE /*_*/ipblocks_tmp RENAME TO /*_*/ipblocks;
+CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only);
+CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user);
+CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
+CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
+CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id);
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-logging-drop-log_comment.sql b/maintenance/sqlite/archives/patch-logging-drop-log_comment.sql
new file mode 100644 (file)
index 0000000..0b53324
--- /dev/null
@@ -0,0 +1,47 @@
+--
+-- patch-logging-drop-log_comment.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/logging_tmp;
+CREATE TABLE /*_*/logging_tmp (
+  log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  log_type varbinary(32) NOT NULL default '',
+  log_action varbinary(32) NOT NULL default '',
+  log_timestamp binary(14) NOT NULL default '19700101000000',
+  log_user int unsigned NOT NULL default 0,
+  log_user_text varchar(255) binary NOT NULL default '',
+  log_actor bigint unsigned NOT NULL DEFAULT 0,
+  log_namespace int NOT NULL default 0,
+  log_title varchar(255) binary NOT NULL default '',
+  log_page int unsigned NULL,
+  log_comment_id bigint unsigned NOT NULL,
+  log_params blob NOT NULL,
+  log_deleted tinyint unsigned NOT NULL default 0
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/logging_tmp (
+       log_id, log_type, log_action, log_timestamp, log_user, log_user_text, log_actor,
+       log_namespace, log_title, log_page, log_comment_id, log_params, log_deleted
+  ) SELECT
+       log_id, log_type, log_action, log_timestamp, log_user, log_user_text, log_actor,
+       log_namespace, log_title, log_page, log_comment_id, log_params, log_deleted
+  FROM /*_*/logging;
+
+DROP TABLE /*_*/logging;
+ALTER TABLE /*_*/logging_tmp RENAME TO /*_*/logging;
+CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp);
+CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp);
+CREATE INDEX /*i*/actor_time ON /*_*/logging (log_actor, log_timestamp);
+CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp);
+CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp);
+CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp);
+CREATE INDEX /*i*/log_actor_type_time ON /*_*/logging (log_actor, log_type, log_timestamp);
+CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp);
+CREATE INDEX /*i*/log_type_action ON /*_*/logging (log_type, log_action, log_timestamp);
+CREATE INDEX /*i*/log_user_text_type_time ON /*_*/logging (log_user_text, log_type, log_timestamp);
+CREATE INDEX /*i*/log_user_text_time ON /*_*/logging (log_user_text, log_timestamp);
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-oldimage-drop-oi_description.sql b/maintenance/sqlite/archives/patch-oldimage-drop-oi_description.sql
new file mode 100644 (file)
index 0000000..1c67cb0
--- /dev/null
@@ -0,0 +1,47 @@
+--
+-- patch-oldimage-drop-oi_description.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/oldimage_tmp;
+CREATE TABLE /*_*/oldimage_tmp (
+  oi_name varchar(255) binary NOT NULL default '',
+  oi_archive_name varchar(255) binary NOT NULL default '',
+  oi_size int unsigned NOT NULL default 0,
+  oi_width int NOT NULL default 0,
+  oi_height int NOT NULL default 0,
+  oi_bits int NOT NULL default 0,
+  oi_description_id bigint unsigned NOT NULL,
+  oi_user int unsigned NOT NULL default 0,
+  oi_user_text varchar(255) binary NOT NULL DEFAULT '',
+  oi_actor bigint unsigned NOT NULL DEFAULT 0,
+  oi_timestamp binary(14) NOT NULL default '',
+  oi_metadata mediumblob NOT NULL,
+  oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
+  oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") NOT NULL default "unknown",
+  oi_minor_mime varbinary(100) NOT NULL default "unknown",
+  oi_deleted tinyint unsigned NOT NULL default 0,
+  oi_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/oldimage_tmp (
+       oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description_id, oi_user, oi_user_text, oi_actor, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1
+  ) SELECT
+       oi_name, oi_archive_name, oi_size, oi_width, oi_height, oi_bits,
+       oi_description_id, oi_user, oi_user_text, oi_actor, oi_timestamp, oi_metadata,
+       oi_media_type, oi_major_mime, oi_minor_mime, oi_deleted, oi_sha1
+  FROM /*_*/oldimage;
+
+DROP TABLE /*_*/oldimage;
+ALTER TABLE /*_*/oldimage_tmp RENAME TO /*_*/oldimage;
+CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp);
+CREATE INDEX /*i*/oi_actor_timestamp ON /*_*/oldimage (oi_actor,oi_timestamp);
+CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
+CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
+CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10));
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-protected_titles-drop-pt_reason.sql b/maintenance/sqlite/archives/patch-protected_titles-drop-pt_reason.sql
new file mode 100644 (file)
index 0000000..731b9d8
--- /dev/null
@@ -0,0 +1,30 @@
+--
+-- patch-protected_titles-drop-pt_reason.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/protected_titles_tmp;
+CREATE TABLE /*_*/protected_titles_tmp (
+  pt_namespace int NOT NULL,
+  pt_title varchar(255) binary NOT NULL,
+  pt_user int unsigned NOT NULL,
+  pt_reason_id bigint unsigned NOT NULL,
+  pt_timestamp binary(14) NOT NULL,
+  pt_expiry varbinary(14) NOT NULL default '',
+  pt_create_perm varbinary(60) NOT NULL,
+  PRIMARY KEY (pt_namespace,pt_title)
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/protected_titles_tmp (
+       pt_namespace, pt_title, pt_user, pt_reason_id, pt_timestamp, pt_expiry, pt_create_perm
+  ) SELECT
+       pt_namespace, pt_title, pt_user, pt_reason_id, pt_timestamp, pt_expiry, pt_create_perm
+  FROM /*_*/protected_titles;
+
+DROP TABLE /*_*/protected_titles;
+ALTER TABLE /*_*/protected_titles_tmp RENAME TO /*_*/protected_titles;
+CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp);
+
+COMMIT;
diff --git a/maintenance/sqlite/archives/patch-recentchanges-drop-rc_comment.sql b/maintenance/sqlite/archives/patch-recentchanges-drop-rc_comment.sql
new file mode 100644 (file)
index 0000000..5ce52da
--- /dev/null
@@ -0,0 +1,63 @@
+--
+-- patch-recentchanges-drop-rc_comment.sql
+--
+-- T166732. Drop old xx_comment fields, and defaults from xx_comment_id fields.
+
+BEGIN;
+
+DROP TABLE IF EXISTS /*_*/recentchanges_tmp;
+CREATE TABLE /*_*/recentchanges_tmp (
+  rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT,
+  rc_timestamp varbinary(14) NOT NULL default '',
+  rc_user int unsigned NOT NULL default 0,
+  rc_user_text varchar(255) binary NOT NULL DEFAULT '',
+  rc_actor bigint unsigned NOT NULL DEFAULT 0,
+  rc_namespace int NOT NULL default 0,
+  rc_title varchar(255) binary NOT NULL default '',
+  rc_comment_id bigint unsigned NOT NULL,
+  rc_minor tinyint unsigned NOT NULL default 0,
+  rc_bot tinyint unsigned NOT NULL default 0,
+  rc_new tinyint unsigned NOT NULL default 0,
+  rc_cur_id int unsigned NOT NULL default 0,
+  rc_this_oldid int unsigned NOT NULL default 0,
+  rc_last_oldid int unsigned NOT NULL default 0,
+  rc_type tinyint unsigned NOT NULL default 0,
+  rc_source varchar(16) binary not null default '',
+  rc_patrolled tinyint unsigned NOT NULL default 0,
+  rc_ip varbinary(40) NOT NULL default '',
+  rc_old_len int,
+  rc_new_len int,
+  rc_deleted tinyint unsigned NOT NULL default 0,
+  rc_logid int unsigned NOT NULL default 0,
+  rc_log_type varbinary(255) NULL default NULL,
+  rc_log_action varbinary(255) NULL default NULL,
+  rc_params blob NULL
+) /*$wgDBTableOptions*/;
+
+INSERT OR IGNORE INTO /*_*/recentchanges_tmp (
+       rc_id, rc_timestamp, rc_user, rc_user_text, rc_actor, rc_namespace, rc_title,
+       rc_comment_id, rc_minor, rc_bot, rc_new, rc_cur_id, rc_this_oldid, rc_last_oldid,
+       rc_type, rc_source, rc_patrolled, rc_ip, rc_old_len, rc_new_len, rc_deleted,
+       rc_logid, rc_log_type, rc_log_action, rc_params
+  ) SELECT
+       rc_id, rc_timestamp, rc_user, rc_user_text, rc_actor, rc_namespace, rc_title,
+       rc_comment_id, rc_minor, rc_bot, rc_new, rc_cur_id, rc_this_oldid, rc_last_oldid,
+       rc_type, rc_source, rc_patrolled, rc_ip, rc_old_len, rc_new_len, rc_deleted,
+       rc_logid, rc_log_type, rc_log_action, rc_params
+  FROM /*_*/recentchanges;
+
+DROP TABLE /*_*/recentchanges;
+ALTER TABLE /*_*/recentchanges_tmp RENAME TO /*_*/recentchanges;
+CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp);
+CREATE INDEX /*i*/rc_namespace_title_timestamp ON /*_*/recentchanges (rc_namespace, rc_title, rc_timestamp);
+CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id);
+CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp);
+CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
+CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
+CREATE INDEX /*i*/rc_ns_actor ON /*_*/recentchanges (rc_namespace, rc_actor);
+CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX /*i*/rc_actor ON /*_*/recentchanges (rc_actor, rc_timestamp);
+CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
+CREATE INDEX /*i*/rc_this_oldid ON /*_*/recentchanges (rc_this_oldid);
+
+COMMIT;
index 260ca98..8a1069e 100644 (file)
@@ -362,7 +362,6 @@ class CompressOld extends Maintenance {
                                $primaryOldid = $revs[$i]->rev_text_id;
 
                                # Get the text of each revision and add it to the object
-                               // phpcs:ignore Generic.CodeAnalysis.ForLoopWithTestFunctionCall
                                for ( $j = 0; $j < $thisChunkSize && $chunk->isHappy(); $j++ ) {
                                        $oldid = $revs[$i + $j]->rev_text_id;
 
index 83ba4e2..dac222f 100644 (file)
@@ -593,8 +593,7 @@ CREATE TABLE /*_*/archive (
   ar_title varchar(255) binary NOT NULL default '',
 
   -- Basic revision stuff...
-  ar_comment varbinary(767) NOT NULL default '', -- Deprecated in favor of ar_comment_id
-  ar_comment_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ar_comment should be used)
+  ar_comment_id bigint unsigned NOT NULL,
   ar_user int unsigned NOT NULL default 0, -- Deprecated in favor of ar_actor
   ar_user_text varchar(255) binary NOT NULL DEFAULT '', -- Deprecated in favor of ar_actor
   ar_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ar_user/ar_user_text should be used)
@@ -1044,12 +1043,8 @@ CREATE TABLE /*_*/ipblocks (
   -- Actor who made the block.
   ipb_by_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that ipb_by/ipb_by_text should be used)
 
-  -- Text comment made by blocker. Deprecated in favor of ipb_reason_id
-  ipb_reason varbinary(767) NOT NULL default '',
-
   -- Key to comment_id. Text comment made by blocker.
-  -- ("DEFAULT 0" is temporary, signaling that ipb_reason should be used)
-  ipb_reason_id bigint unsigned NOT NULL DEFAULT 0,
+  ipb_reason_id bigint unsigned NOT NULL,
 
   -- Creation (or refresh) date in standard YMDHMS form.
   -- IP blocks expire automatically.
@@ -1180,10 +1175,7 @@ CREATE TABLE /*_*/image (
 
   -- Description field as entered by the uploader.
   -- This is displayed in image upload history and logs.
-  -- Deprecated in favor of img_description_id.
-  img_description varbinary(767) NOT NULL default '',
-
-  img_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that img_description should be used)
+  img_description_id bigint unsigned NOT NULL,
 
   -- user_id and user_name of uploader.
   -- Deprecated in favor of img_actor.
@@ -1233,8 +1225,7 @@ CREATE TABLE /*_*/oldimage (
   oi_width int NOT NULL default 0,
   oi_height int NOT NULL default 0,
   oi_bits int NOT NULL default 0,
-  oi_description varbinary(767) NOT NULL default '', -- Deprecated.
-  oi_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that oi_description should be used)
+  oi_description_id bigint unsigned NOT NULL,
   oi_user int unsigned NOT NULL default 0, -- Deprecated in favor of oi_actor
   oi_user_text varchar(255) binary NOT NULL DEFAULT '', -- Deprecated in favor of oi_actor
   oi_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that oi_user/oi_user_text should be used)
@@ -1284,8 +1275,7 @@ CREATE TABLE /*_*/filearchive (
   -- Deletion information, if this file is deleted.
   fa_deleted_user int,
   fa_deleted_timestamp binary(14) default '',
-  fa_deleted_reason varbinary(767) default '', -- Deprecated
-  fa_deleted_reason_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_deleted_reason should be used)
+  fa_deleted_reason_id bigint unsigned NOT NULL,
 
   -- Duped fields from image
   fa_size int unsigned default 0,
@@ -1296,8 +1286,7 @@ CREATE TABLE /*_*/filearchive (
   fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE", "3D") default NULL,
   fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart", "chemical") default "unknown",
   fa_minor_mime varbinary(100) default "unknown",
-  fa_description varbinary(767) default '', -- Deprecated
-  fa_description_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_description should be used)
+  fa_description_id bigint unsigned NOT NULL,
   fa_user int unsigned default 0, -- Deprecated in favor of fa_actor
   fa_user_text varchar(255) binary DEFAULT '', -- Deprecated in favor of fa_actor
   fa_actor bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that fa_user/fa_user_text should be used)
@@ -1398,8 +1387,7 @@ CREATE TABLE /*_*/recentchanges (
   rc_title varchar(255) binary NOT NULL default '',
 
   -- as in revision...
-  rc_comment varbinary(767) NOT NULL default '', -- Deprecated.
-  rc_comment_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that rc_comment should be used)
+  rc_comment_id bigint unsigned NOT NULL,
   rc_minor tinyint unsigned NOT NULL default 0,
 
   -- Edits by user accounts with the 'bot' rights key are
@@ -1622,13 +1610,8 @@ CREATE TABLE /*_*/logging (
   log_title varchar(255) binary NOT NULL default '',
   log_page int unsigned NULL,
 
-  -- Freeform text. Interpreted as edit history comments.
-  -- Deprecated in favor of log_comment_id.
-  log_comment varbinary(767) NOT NULL default '',
-
   -- Key to comment_id. Comment summarizing the change.
-  -- ("DEFAULT 0" is temporary, signaling that log_comment should be used)
-  log_comment_id bigint unsigned NOT NULL DEFAULT 0,
+  log_comment_id bigint unsigned NOT NULL,
 
   -- miscellaneous parameters:
   -- LF separated list (old system) or serialized PHP array (new system)
@@ -1805,8 +1788,7 @@ CREATE TABLE /*_*/protected_titles (
   pt_namespace int NOT NULL,
   pt_title varchar(255) binary NOT NULL,
   pt_user int unsigned NOT NULL,
-  pt_reason varbinary(767) default '', -- Deprecated.
-  pt_reason_id bigint unsigned NOT NULL DEFAULT 0, -- ("DEFAULT 0" is temporary, signaling that pt_reason should be used)
+  pt_reason_id bigint unsigned NOT NULL,
   pt_timestamp binary(14) NOT NULL,
   pt_expiry varbinary(14) NOT NULL default '',
   pt_create_perm varbinary(60) NOT NULL,
index b7e8c1c..25ce3ce 100644 (file)
@@ -26,6 +26,12 @@ if ( PHP_SAPI != 'cli' ) {
        die( "This script can only be run from the command line.\n" );
 }
 
+// class Collator is provided by the intl extension.
+// It is only suggested in composer.json, so remind here when not loaded.
+if ( !extension_loaded( 'intl' ) ) {
+       die( "This script needs the 'intl' extension to be loaded." );
+}
+
 $CREDITS = 'CREDITS';
 $START_CONTRIBUTORS = '<!-- BEGIN CONTRIBUTOR LIST -->';
 $END_CONTRIBUTORS = '<!-- END CONTRIBUTOR LIST -->';
index 69c9265..257c6be 100644 (file)
@@ -163,7 +163,7 @@ class UserDupes {
 
        /**
         * We don't want anybody to mess with our stuff...
-        * @access private
+        * @private
         */
        function lock() {
                $set = [ 'user', 'revision' ];
@@ -178,7 +178,7 @@ class UserDupes {
        }
 
        /**
-        * @access private
+        * @private
         */
        function unlock() {
                $this->db->query( "UNLOCK TABLES", __METHOD__ );
@@ -187,7 +187,7 @@ class UserDupes {
        /**
         * Grab usernames for which multiple records are present in the database.
         * @return array
-        * @access private
+        * @private
         */
        function getDupes() {
                $user = $this->db->tableName( 'user' );
@@ -211,7 +211,7 @@ class UserDupes {
         * for edits. If the dupes have no edits, we can safely remove them.
         * @param string $name
         * @param bool $doDelete
-        * @access private
+        * @private
         */
        function examine( $name, $doDelete ) {
                $result = $this->db->select( 'user',
@@ -260,7 +260,7 @@ class UserDupes {
         * where it might show up...
         * @param int $userid
         * @return int
-        * @access private
+        * @private
         */
        function editCount( $userid ) {
                return intval( $this->db->selectField(
@@ -273,7 +273,7 @@ class UserDupes {
        /**
         * @param int $from
         * @param int $to
-        * @access private
+        * @private
         */
        function reassignEdits( $from, $to ) {
                $this->out( 'reassigning... ' );
@@ -287,7 +287,7 @@ class UserDupes {
        /**
         * Remove a user account line.
         * @param int $userid
-        * @access private
+        * @private
         */
        function trimAccount( $userid ) {
                $this->out( "deleting..." );
index c0570cf..e143750 100644 (file)
@@ -10,7 +10,7 @@
     "selenium-test": "wdio ./tests/selenium/wdio.conf.js"
   },
   "devDependencies": {
-    "eslint-config-wikimedia": "0.10.0",
+    "eslint-config-wikimedia": "0.10.1",
     "grunt": "1.0.3",
     "grunt-banana-checker": "0.6.0",
     "grunt-contrib-copy": "1.0.0",
@@ -19,6 +19,7 @@
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "3.0.0",
     "grunt-stylelint": "0.10.1",
+    "grunt-svgmin": "5.0.0",
     "karma": "3.0.0",
     "karma-chrome-launcher": "2.2.0",
     "karma-firefox-launcher": "1.1.0",
index 9464cf7..d62f3e3 100644 (file)
@@ -24,6 +24,8 @@ if ( !defined( 'MEDIAWIKI' ) ) {
        die( 'Not an entry point.' );
 }
 
+global $wgResourceBasePath;
+
 return [
 
        /**
@@ -227,7 +229,6 @@ return [
                'scripts' => 'resources/lib/jquery.fullscreen.js',
        ],
        'jquery.getAttrs' => [
-               'targets' => [ 'desktop', 'mobile' ],
                'scripts' => 'resources/src/jquery/jquery.getAttrs.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
@@ -320,19 +321,25 @@ return [
        'jquery.tablesorter' => [
                'targets' => [ 'desktop', 'mobile' ],
                'scripts' => 'resources/src/jquery.tablesorter/jquery.tablesorter.js',
-               'styles' => 'resources/src/jquery.tablesorter/jquery.tablesorter.less',
                'messages' => [ 'sort-descending', 'sort-ascending' ],
                'dependencies' => [
+                       'jquery.tablesorter.styles',
                        'mediawiki.RegExp',
                        'mediawiki.language.months',
                ],
        ],
+       'jquery.tablesorter.styles' => [
+               'targets' => [ 'desktop', 'mobile' ],
+               'styles' => 'resources/src/jquery.tablesorter.styles/jquery.tablesorter.styles.less',
+       ],
        'jquery.textSelection' => [
                'scripts' => 'resources/src/jquery/jquery.textSelection.js',
                'dependencies' => 'jquery.client',
                'targets' => [ 'mobile', 'desktop' ],
        ],
        'jquery.throttle-debounce' => [
+               'deprecated' => 'Please use OO.ui.throttle/debounce instead. See '
+                       . 'https://phabricator.wikimedia.org/T213426',
                'scripts' => 'resources/lib/jquery.ba-throttle-debounce.js',
                'targets' => [ 'desktop', 'mobile' ],
        ],
@@ -634,6 +641,7 @@ return [
                'deprecated' => true,
                'scripts' => 'resources/lib/jquery.ui/jquery.ui.widget.js',
                'group' => 'jquery.ui',
+               'targets' => [ 'desktop', 'mobile' ],
        ],
        // Effects
        'jquery.effects.core' => [
@@ -1171,14 +1179,15 @@ return [
                        'upload-foreign-cant-upload',
                ]
        ],
-       'mediawiki.ForeignStructuredUpload.config' => [
-               'class' => ResourceLoaderUploadDialogModule::class,
-       ],
        'mediawiki.ForeignStructuredUpload' => [
-               'scripts' => 'resources/src/mediawiki.ForeignStructuredUpload.js',
+               'localBasePath' => "$IP/resources/src",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src",
+               'packageFiles' => [
+                       'mediawiki.ForeignStructuredUpload.js',
+                       'config.json' => [ 'config' => [ 'UploadDialog' ] ],
+               ],
                'dependencies' => [
                        'mediawiki.ForeignUpload',
-                       'mediawiki.ForeignStructuredUpload.config',
                ],
                'messages' => [
                        'upload-foreign-cant-load-config',
@@ -1321,8 +1330,12 @@ return [
                ]
        ],
        'mediawiki.util' => [
-               'class' => ResourceLoaderMediaWikiUtilModule::class,
-               'scripts' => 'resources/src/mediawiki.util.js',
+               'localBasePath' => "$IP/resources/src",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src",
+               'packageFiles' => [
+                       'mediawiki.util.js',
+                       'config.json' => [ 'config' => [ 'FragmentMode' ] ],
+               ],
                'dependencies' => [
                        'jquery.accessKeyLabel',
                        'mediawiki.RegExp',
@@ -1564,9 +1577,31 @@ return [
        ],
 
        'mediawiki.jqueryMsg' => [
-               // Add data for mediawiki.jqueryMsg, such as allowed tags
-               'class' => ResourceLoaderJqueryMsgModule::class,
-               'scripts' => 'resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js',
+               'localBasePath' => "$IP/resources/src/mediawiki.jqueryMsg",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.jqueryMsg",
+               'packageFiles' => [
+                       'mediawiki.jqueryMsg.js',
+                       'parserDefaults.json' => [ 'callback' => function ( ResourceLoaderContext $context ) {
+                               $tagData = Sanitizer::getRecognizedTagData();
+                               $allowedHtmlElements = array_merge(
+                                       array_keys( $tagData['htmlpairs'] ),
+                                       array_diff(
+                                               array_keys( $tagData['htmlsingle'] ),
+                                               array_keys( $tagData['htmlsingleonly'] )
+                                       )
+                               );
+
+                               $magicWords = [
+                                       'SITENAME' => $context->getConfig()->get( 'Sitename' ),
+                               ];
+                               Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] );
+
+                               return [
+                                       'allowedHtmlElements' => $allowedHtmlElements,
+                                       'magic' => $magicWords,
+                               ];
+                       } ],
+               ],
                'dependencies' => [
                        'mediawiki.util',
                        'mediawiki.language',
@@ -1586,10 +1621,54 @@ return [
                )
        ],
 
-       'mediawiki.language.names' => [ 'class' => ResourceLoaderLanguageNamesModule::class ],
+       'mediawiki.language.names' => [
+               'localBasePath' => "$IP/resources/src/mediawiki.language",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.language",
+               'packageFiles' => [
+                       'mediawiki.language.names.js',
+                       'names.json' => [ 'callback' => function ( ResourceLoaderContext $context ) {
+                               return Language::fetchLanguageNames( $context->getLanguage(), 'all' );
+                       } ],
+               ],
+               'dependencies' => 'mediawiki.language',
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
 
        'mediawiki.language.specialCharacters' => [
-               'class' => ResourceLoaderSpecialCharacterDataModule::class
+               'localBasePath' => "$IP/resources/src/mediawiki.language",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.language",
+               'packageFiles' => [
+                       'mediawiki.language.specialCharacters.js',
+                       'specialcharacters.json'
+               ],
+               'dependencies' => 'mediawiki.language',
+               'targets' => [ 'desktop', 'mobile' ],
+               'messages' => [
+                       'special-characters-group-latin',
+                       'special-characters-group-latinextended',
+                       'special-characters-group-ipa',
+                       'special-characters-group-symbols',
+                       'special-characters-group-greek',
+                       'special-characters-group-greekextended',
+                       'special-characters-group-cyrillic',
+                       'special-characters-group-arabic',
+                       'special-characters-group-arabicextended',
+                       'special-characters-group-persian',
+                       'special-characters-group-hebrew',
+                       'special-characters-group-bangla',
+                       'special-characters-group-tamil',
+                       'special-characters-group-telugu',
+                       'special-characters-group-sinhala',
+                       'special-characters-group-devanagari',
+                       'special-characters-group-gujarati',
+                       'special-characters-group-thai',
+                       'special-characters-group-lao',
+                       'special-characters-group-khmer',
+                       'special-characters-group-canadianaboriginal',
+                       'special-characters-title-endash',
+                       'special-characters-title-emdash',
+                       'special-characters-title-minus'
+               ]
        ],
 
        /* MediaWiki Libs */
@@ -1721,17 +1800,20 @@ return [
                ],
        ],
        'mediawiki.rcfilters.filters.dm' => [
-               'scripts' => [
-                       'resources/src/mediawiki.rcfilters/mw.rcfilters.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js',
-                       'resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js',
-                       'resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js',
-                       'resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js',
+               'localBasePath' => "$IP/resources/src/mediawiki.rcfilters",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.rcfilters",
+               'packageFiles' => [
+                       'mw.rcfilters.js',
+                       'Controller.js',
+                       'UriProcessor.js',
+                       'dm/ChangesListViewModel.js',
+                       'dm/FilterGroup.js',
+                       'dm/FilterItem.js',
+                       'dm/FiltersViewModel.js',
+                       'dm/ItemModel.js',
+                       'dm/SavedQueriesModel.js',
+                       'dm/SavedQueryItemModel.js',
+                       'config.json' => [ 'config' => [ 'StructuredChangeFiltersLiveUpdatePollingRate' ] ],
                ],
                'dependencies' => [
                        'mediawiki.String',
@@ -1748,79 +1830,82 @@ return [
                ],
        ],
        'mediawiki.rcfilters.filters.ui' => [
-               'scripts' => [
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.GroupWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MainWrapperWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ViewSwitchWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListItemWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightPopupWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js',
-                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js',
-                       'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
-                       'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
+               'localBasePath' => "$IP/resources/src/mediawiki.rcfilters",
+               'remoteBasePath' => "$wgResourceBasePath/resources/src/mediawiki.rcfilters",
+               'packageFiles' => [
+                       'mw.rcfilters.init.js',
+                       'HighlightColors.js',
+                       'ui/GroupWidget.js',
+                       'ui/CheckboxInputWidget.js',
+                       'ui/FilterTagMultiselectWidget.js',
+                       'ui/ItemMenuOptionWidget.js',
+                       'ui/FilterMenuOptionWidget.js',
+                       'ui/FilterMenuSectionOptionWidget.js',
+                       'ui/TagItemWidget.js',
+                       'ui/FilterTagItemWidget.js',
+                       'ui/FilterMenuHeaderWidget.js',
+                       'ui/MenuSelectWidget.js',
+                       'ui/MainWrapperWidget.js',
+                       'ui/ViewSwitchWidget.js',
+                       'ui/ValuePickerWidget.js',
+                       'ui/ChangesLimitPopupWidget.js',
+                       'ui/ChangesLimitAndDateButtonWidget.js',
+                       'ui/DatePopupWidget.js',
+                       'ui/FilterWrapperWidget.js',
+                       'ui/ChangesListWrapperWidget.js',
+                       'ui/SavedLinksListWidget.js',
+                       'ui/SavedLinksListItemWidget.js',
+                       'ui/SaveFiltersPopupButtonWidget.js',
+                       'ui/FormWrapperWidget.js',
+                       'ui/FilterItemHighlightButton.js',
+                       'ui/HighlightPopupWidget.js',
+                       'ui/HighlightColorPickerWidget.js',
+                       'ui/LiveUpdateButtonWidget.js',
+                       'ui/MarkSeenButtonWidget.js',
+                       'ui/RcTopSectionWidget.js',
+                       'ui/RclTopSectionWidget.js',
+                       'ui/RclTargetPageWidget.js',
+                       'ui/RclToOrFromWidget.js',
+                       'ui/WatchlistTopSectionWidget.js',
+                       'config.json' => [ 'callback' => 'ChangesListSpecialPage::getRcFiltersConfigVars' ],
                ],
                'styles' => [
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.mixins.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ItemMenuOptionWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.TagItemWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.MenuSelectWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ViewSwitchWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesLimitPopupWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListItemWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclTargetPageWidget.less',
-                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less',
+                       'styles/mw.rcfilters.mixins.less',
+                       'styles/mw.rcfilters.variables.less',
+                       'styles/mw.rcfilters.ui.less',
+                       'styles/mw.rcfilters.ui.Overlay.less',
+                       'styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less',
+                       'styles/mw.rcfilters.ui.ItemMenuOptionWidget.less',
+                       'styles/mw.rcfilters.ui.FilterMenuOptionWidget.less',
+                       'styles/mw.rcfilters.ui.FilterMenuSectionOptionWidget.less',
+                       'styles/mw.rcfilters.ui.TagItemWidget.less',
+                       'styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less',
+                       'styles/mw.rcfilters.ui.MenuSelectWidget.less',
+                       'styles/mw.rcfilters.ui.ViewSwitchWidget.less',
+                       'styles/mw.rcfilters.ui.ValuePickerWidget.less',
+                       'styles/mw.rcfilters.ui.ChangesLimitPopupWidget.less',
+                       'styles/mw.rcfilters.ui.DatePopupWidget.less',
+                       'styles/mw.rcfilters.ui.FilterWrapperWidget.less',
+                       'styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
+                       'styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
+                       'styles/mw.rcfilters.ui.FilterItemHighlightButton.less',
+                       'styles/mw.rcfilters.ui.SavedLinksListWidget.less',
+                       'styles/mw.rcfilters.ui.SavedLinksListItemWidget.less',
+                       'styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less',
+                       'styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less',
+                       'styles/mw.rcfilters.ui.RcTopSectionWidget.less',
+                       'styles/mw.rcfilters.ui.RclToOrFromWidget.less',
+                       'styles/mw.rcfilters.ui.RclTargetPageWidget.less',
+                       'styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less',
                ],
                'skinStyles' => [
                        'vector' => [
-                               'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.vector.less',
+                               'styles/mw.rcfilters.ui.Overlay.vector.less',
                        ],
                        'monobook' => [
-                               'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.monobook.less',
-                               'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.monobook.less',
-                               'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuOptionWidget.monobook.less',
+                               'styles/mw.rcfilters.ui.Overlay.monobook.less',
+                               'styles/mw.rcfilters.ui.CapsuleItemWidget.monobook.less',
+                               'styles/mw.rcfilters.ui.FilterMenuOptionWidget.monobook.less',
                        ],
                ],
                'messages' => [
index 9f186ca..1061a7d 100644 (file)
@@ -1,9 +1,10 @@
-Trevor Parscal <trevorparscal@gmail.com>
-Timo Tijhof <krinklemail@gmail.com>
-Roan Kattouw <roan.kattouw@gmail.com>
-Derk-Jan Hartman <hartman.wiki@gmail.com>
+Alexander Monk <krenair@gmail.com>
 Bartosz Dziewoński <matma.rex@gmail.com>
-Rob Moen <rmoen@wikimedia.org>
+Brion Vibber <brion@wikimedia.org>
+Derk-Jan Hartman <hartman@videolan.org>
 Ed Sanders <esanders@wikimedia.org>
-Alex Monk <krenair@gmail.com>
 James D. Forrester <jforrester@wikimedia.org>
+Roan Kattouw <roan.kattouw@gmail.com>
+Rob Moen <rmoen@mediawiki.org>
+Timo Tijhof <krinklemail@gmail.com>
+Trevor Parscal <trevorparscal@gmail.com>
index cfe2d29..79f6174 100644 (file)
@@ -1,8 +1,8 @@
 /*!
- * jQuery Client v2.0.1
+ * jQuery Client v2.0.2
  * https://www.mediawiki.org/wiki/JQuery_Client
  *
- * Copyright 2010-2015 jquery-client maintainers and other contributors.
+ * Copyright 2010-2019 jquery-client maintainers and other contributors.
  * Released under the MIT license
  * http://jquery-client.mit-license.org
  */
@@ -13,7 +13,7 @@
  * @class jQuery.client
  * @singleton
  */
-( function ( $ ) {
+( function () {
 
        /**
         * @private
@@ -51,6 +51,7 @@
                                return profileCache[ nav.userAgent + '|' + nav.platform ];
                        }
 
+                       // eslint-disable-next-line vars-on-top
                        var
                                versionNumber,
                                key = nav.userAgent + '|' + nav.platform,
                                        [ 'Minefield', 'Firefox' ],
                                        // This helps keep different versions consistent
                                        [ 'Navigator', 'Netscape' ],
-                                       // This prevents version extraction issues, otherwise translation would happen later
+                                       // This prevents version extraction issues,
+                                       // otherwise translation would happen later
                                        [ 'PLAYSTATION 3', 'PS3' ]
                                ],
-                               // Strings which precede a version number in a user agent string - combined and used as
-                               // match 1 in version detection
+                               // Strings which precede a version number in a user agent string - combined and
+                               // used as match 1 in version detection
                                versionPrefixes = [
                                        'camino', 'chrome', 'firefox', 'iceweasel', 'netscape', 'netscape6', 'opera', 'version', 'konqueror',
                                        'lynx', 'msie', 'safari', 'ps3', 'android'
                                ],
-                               // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
+                               // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual
+                               // version number
                                versionSuffix = '(\\/|\\;?\\s|)([a-z0-9\\.\\+]*?)(\\;|dev|rel|\\)|\\s|$)',
                                // Names of known browsers
                                names = [
                                // Translations for conforming operating system names
                                platformTranslations = [ [ 'sunos', 'solaris' ], [ 'wow64', 'win' ] ],
 
-                               /**
-                                * Performs multiple replacements on a string
-                                * @ignore
-                                */
+                               // Performs multiple replacements on a string
                                translate = function ( source, translations ) {
                                        var i;
                                        for ( i = 0; i < translations.length; i++ ) {
                                platform = uk,
                                version = x;
 
-                       if ( match = new RegExp( '(' + wildUserAgents.join( '|' ) + ')' ).exec( ua ) ) {
-                               // Takes a userAgent string and translates given text into something we can more easily work with
+                       if ( ( match = new RegExp( '(' + wildUserAgents.join( '|' ) + ')' ).exec( ua ) ) ) {
+                               // Takes a userAgent string and translates given text into something we can more
+                               // easily work with
                                ua = translate( ua, userAgentTranslations );
                        }
                        // Everything will be in lowercase from now on
                        ua = ua.toLowerCase();
 
+                       // Firefox Mobile: Remove 'Android' identifier so it matches to 'Firefox' instead
+                       if ( ua.match( /android/ ) && ua.match( /firefox/ ) ) {
+                               ua = ua.replace( new RegExp( 'android' + versionSuffix ), '' );
+                       }
+
                        // Extraction
 
-                       if ( match = new RegExp( '(' + names.join( '|' ) + ')' ).exec( ua ) ) {
+                       if ( ( match = new RegExp( '(' + names.join( '|' ) + ')' ).exec( ua ) ) ) {
                                name = translate( match[ 1 ], nameTranslations );
                        }
-                       if ( match = new RegExp( '(' + layouts.join( '|' ) + ')' ).exec( ua ) ) {
+                       if ( ( match = new RegExp( '(' + layouts.join( '|' ) + ')' ).exec( ua ) ) ) {
                                layout = translate( match[ 1 ], layoutTranslations );
                        }
-                       if ( match = new RegExp( '(' + layoutVersions.join( '|' ) + ')\\/(\\d+)' ).exec( ua ) ) {
+                       if ( ( match = new RegExp( '(' + layoutVersions.join( '|' ) + ')\\/(\\d+)' ).exec( ua ) ) ) {
                                layoutversion = parseInt( match[ 2 ], 10 );
                        }
-                       if ( match = new RegExp( '(' + platforms.join( '|' ) + ')' ).exec( nav.platform.toLowerCase() ) ) {
+                       if ( ( match = new RegExp( '(' + platforms.join( '|' ) + ')' ).exec( nav.platform.toLowerCase() ) ) ) {
                                platform = translate( match[ 1 ], platformTranslations );
                        }
-                       if ( match = new RegExp( '(' + versionPrefixes.join( '|' ) + ')' + versionSuffix ).exec( ua ) ) {
+                       if ( ( match = new RegExp( '(' + versionPrefixes.join( '|' ) + ')' + versionSuffix ).exec( ua ) ) ) {
                                version = match[ 3 ];
                        }
 
                                layoutversion = parseInt( match[ 1 ], 10 );
                        }
                        // And Amazon Silk's lies about being Android on mobile or Safari on desktop
-                       if ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) {
+                       if ( ( match = ua.match( /\bsilk\/([0-9.\-_]*)/ ) ) ) {
                                if ( match[ 1 ] ) {
                                        name = 'silk';
                                        version = match[ 1 ];
                        versionNumber = parseFloat( version, 10 ) || 0.0;
 
                        // Caching
-
-                       return profileCache[ key ] = {
+                       profileCache[ key ] = {
                                name: name,
                                layout: layout,
                                layoutVersion: layoutversion,
                                versionBase: ( version !== x ? Math.floor( versionNumber ).toString() : x ),
                                versionNumber: versionNumber
                        };
+
+                       return profileCache[ key ];
                },
 
                /**
                 *
                 * @param {Object} map Browser support map
                 * @param {Object} [profile] A client-profile object
-                * @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched, otherwise
-                * returns true if the browser is not found.
+                * @param {boolean} [exactMatchOnly=false] Only return true if the browser is matched,
+                *  otherwise returns true if the browser is not found.
                 *
                 * @return {boolean} The current browser is in the support map
                 */
                test: function ( map, profile, exactMatchOnly ) {
-                       /* eslint-disable no-eval */
-
                        var conditions, dir, i, op, val, j, pieceVersion, pieceVal, compare;
                        profile = $.isPlainObject( profile ) ? profile : $.client.profile();
                        if ( map.ltr && map.rtl ) {
-                               dir = $( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr';
+                               dir = $( document.body ).is( '.rtl' ) ? 'rtl' : 'ltr';
                                map = map[ dir ];
                        }
-                       // Check over each browser condition to determine if we are running in a compatible client
+                       // Check over each browser condition to determine if we are running in a
+                       // compatible client
                        if ( typeof map !== 'object' || map[ profile.name ] === undefined ) {
                                // Not found, return true if exactMatchOnly not set, false otherwise
                                return !exactMatchOnly;
                                op = conditions[ i ][ 0 ];
                                val = conditions[ i ][ 1 ];
                                if ( typeof val === 'string' ) {
-                                       // Perform a component-wise comparison of versions, similar to PHP's version_compare
-                                       // but simpler. '1.11' is larger than '1.2'.
+                                       // Perform a component-wise comparison of versions, similar to
+                                       // PHP's version_compare but simpler. '1.11' is larger than '1.2'.
                                        pieceVersion = profile.version.toString().split( '.' );
                                        pieceVal = val.split( '.' );
                                        // Extend with zeroes to equal length
                                                }
                                        }
                                        // compare will be -1, 0 or 1, depending on comparison result
+                                       // eslint-disable-next-line no-eval
                                        if ( !( eval( String( compare + op + '0' ) ) ) ) {
                                                return false;
                                        }
                                } else if ( typeof val === 'number' ) {
+                                       // eslint-disable-next-line no-eval
                                        if ( !( eval( 'profile.versionNumber' + op + val ) ) ) {
                                                return false;
                                        }
                        return true;
                }
        };
-}( jQuery ) );
+}() );
index 5390c84..a406b57 100644 (file)
@@ -1 +1,7 @@
+Ed Sanders <esanders@wikimedia.org>
+James D. Forrester <jforrester@wikimedia.org>
 Jon Robson <jdlrobson@gmail.com>
+Kunal Mehta <legoktm@member.fsf.org>
+MarcoAurelio <maurelio@tools.wmflabs.org>
+Prateek Saxena <prtksxna@gmail.com>
+Timo Tijhof <krinklemail@gmail.com>
diff --git a/resources/lib/oojs-router/History.md b/resources/lib/oojs-router/History.md
new file mode 100644 (file)
index 0000000..c3889cf
--- /dev/null
@@ -0,0 +1,23 @@
+# OOjs Router Release History
+## v0.2.0 / 2019-02-06
+* build: Misc clean up (Timo Tijhof)
+* Code style fixes and refactoring (Ed Sanders)
+* build: Switch to ESLint (Timo Tijhof)
+* Unit tests (Jon Robson)
+* Add .gitreview (Christian Aistleitner)
+* Remove .arcconfig & .arclint (MarcoAurelio)
+* Add jsduck config (Prateek Saxena)
+* build: Update devDependencies (Ed Sanders)
+* Use standard coverage reporting configuration (Kunal Mehta)
+* Update eslint-config-wikimedia to 0.9.0 (Ed Sanders)
+* build: Update devDependencies (Ed Sanders)
+* build: Structure updates (Timo Tijhof)
+* OOjs Router method to provide access to replaceState (Jon Robson)
+* build: Switch from prepublish to prepare for modern npm (James D. Forrester)
+* build: Bump package-lock.json for npm audit (James D. Forrester)
+* build: Add AUTHORS.txt to package manifest (James D. Forrester)
+* AUTHORS.txt: Add everyone from `git shortlog` (James D. Forrester)
+
+## v0.1.0 / 2016-05-05
+* Initial commit (Jon Robson)
+* Add build, CI infrastructure (James D. Forrester)
diff --git a/resources/lib/oojs-router/LICENSE b/resources/lib/oojs-router/LICENSE
new file mode 100644 (file)
index 0000000..acbe708
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright 2011-2016 OOjs Team and other contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/resources/lib/oojs-router/LICENSE-MIT b/resources/lib/oojs-router/LICENSE-MIT
deleted file mode 100644 (file)
index acbe708..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright 2011-2016 OOjs Team and other contributors.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
index b136923..111d0e8 100644 (file)
 /*!
- * OOjs Router v0.1.0
- * https://www.mediawiki.org/wiki/OOjs
+ * OOjs Router v0.2.0
+ * https://www.mediawiki.org/wiki/OOjs_Router
  *
- * Copyright 2011-2016 OOjs Team and other contributors.
+ * Copyright 2011-2019 OOjs Team and other contributors.
  * Released under the MIT license
- * http://oojs-router.mit-license.org
+ * http://oojs.mit-license.org
  *
- * Date: 2016-05-05T19:27:58Z
+ * Date: 2019-02-06T21:26:01Z
  */
 ( function ( $ ) {
 
 'use strict';
 
-/**
- * Does hash match entry.path? If it does apply the
- * callback for the Entry object.
- *
- * @method
- * @private
- * @ignore
- * @param {string} hash string to match
- * @param {Object} entry Entry object
- * @return {boolean} Whether hash matches entry.path
- */
-function matchRoute( hash, entry ) {
-       var match = hash.match( entry.path );
-       if ( match ) {
-               entry.callback.apply( this, match.slice( 1 ) );
-               return true;
-       }
-       return false;
-}
-
 /**
  * Provides navigation routing and location information
  *
- * @class Router
- * @mixins OO.EventEmitter
+ * @class OO.Router
+ * @extends OO.Registry
  */
-function Router() {
-       var self = this;
-       OO.EventEmitter.call( this );
-       // use an object instead of an array for routes so that we don't
-       // duplicate entries that already exist
-       this.routes = {};
+OO.Router = function OoRouter() {
+       var router = this;
+
+       // Parent constructor
+       OO.Router.parent.call( this );
+
        this.enabled = true;
        this.oldHash = this.getPath();
 
        $( window ).on( 'popstate', function () {
-               self.emit( 'popstate' );
+               router.emit( 'popstate' );
        } );
 
        $( window ).on( 'hashchange', function () {
-               self.emit( 'hashchange' );
+               router.emit( 'hashchange' );
        } );
 
        this.on( 'hashchange', function () {
-               // ev.originalEvent.newURL is undefined on Android 2.x
-               var routeEv;
+               // event.originalEvent.newURL is undefined on Android 2.x
+               var routeEvent;
 
-               if ( self.enabled ) {
-                       routeEv = $.Event( 'route', {
-                               path: self.getPath()
+               if ( router.enabled ) {
+                       routeEvent = $.Event( 'route', {
+                               path: router.getPath()
                        } );
-                       self.emit( 'route', routeEv );
+                       router.emit( 'route', routeEvent );
 
-                       if ( !routeEv.isDefaultPrevented() ) {
-                               self.checkRoute();
+                       if ( !routeEvent.isDefaultPrevented() ) {
+                               router.checkRoute();
                        } else {
                                // if route was prevented, ignore the next hash change and revert the
                                // hash to its old value
-                               self.enabled = false;
-                               self.navigate( self.oldHash );
+                               router.enabled = false;
+                               router.navigate( router.oldHash );
                        }
                } else {
-                       self.enabled = true;
+                       router.enabled = true;
                }
 
-               self.oldHash = self.getPath();
+               router.oldHash = router.getPath();
        } );
-}
-OO.mixinClass( Router, OO.EventEmitter );
+};
+
+/* Inheritance */
+
+OO.inheritClass( OO.Router, OO.Registry );
+
+/* Events */
 
 /**
- * Check the current route and run appropriate callback if it matches.
+ * @event popstate
+ */
+
+/**
+ * @event hashchange
+ */
+
+/**
+ * @event route
+ * @param {jQuery.Event} routeEvent
+ */
+
+/* Static Methods */
+
+/**
+ * Determine if current browser supports this router
  *
- * @method
+ * @return {boolean} The browser is supported
  */
-Router.prototype.checkRoute = function () {
-       var hash = this.getPath();
+OO.Router.static.isSupported = function () {
+       return 'onhashchange' in window;
+};
 
-       $.each( this.routes, function ( id, entry ) {
-               return !matchRoute( hash, entry );
-       } );
+/* Methods */
+
+/**
+ * Check the current route and run appropriate callback if it matches.
+ */
+OO.Router.prototype.checkRoute = function () {
+       var id, entry, match,
+               hash = this.getPath();
+
+       for ( id in this.registry ) {
+               entry = this.registry[ id ];
+               match = hash.match( entry.path );
+               if ( match ) {
+                       entry.callback.apply( this, match.slice( 1 ) );
+                       return;
+               }
+       }
 };
 
 /**
  * Bind a specific callback to a hash-based route, e.g.
  *
  *     @example
- *     route( 'alert', function () { alert( 'something' ); } );
- *     route( /hi-(.*)/, function ( name ) { alert( 'Hi ' + name ) } );
+ *     addRoute( 'alert', function () { alert( 'something' ); } );
+ *     addRoute( /hi-(.*)/, function ( name ) { alert( 'Hi ' + name ) } );
+ *
  * Note that after defining all available routes it is up to the caller
  * to check the existing route via the checkRoute method.
  *
- * @method
- * @param {Object} path string or RegExp to match.
+ * @param {string|RegExp} path Path to match, string or regular expression
  * @param {Function} callback Callback to be run when hash changes to one
  * that matches.
  */
-Router.prototype.route = function ( path, callback ) {
+OO.Router.prototype.addRoute = function ( path, callback ) {
        var entry = {
                path: typeof path === 'string' ?
-                       new RegExp( '^' + path.replace( /[\\^$*+?.()|[\]{}]/g, '\\$&' ) + '$' )
-                       path,
+                       new RegExp( '^' + path.replace( /[\\^$*+?.()|[\]{}]/g, '\\$&' ) + '$' ) :
+                       path,
                callback: callback
        };
-       this.routes[ entry.path ] = entry;
+       this.register( entry.path.toString(), entry );
 };
 
+/**
+ * @deprecated Use #addRoute
+ */
+OO.Router.prototype.route = OO.Router.prototype.addRoute;
+
 /**
  * Navigate to a specific route.
  *
- * @method
- * @param {string} path string with a route (hash without #).
+ * @param {string} title of new page
+ * @param {Object} options
+ * @param {string} options.path e.g. '/path/' or '/path/#foo'
+ * @param {boolean} options.useReplaceState Set replaceStateState to use pushState when you want to
+ *   avoid long history queues.
  */
-Router.prototype.navigate = function ( path ) {
-       var history = window.history;
+OO.Router.prototype.navigateTo = function ( title, options ) {
+       if ( options.useReplaceState ) {
+               history.replaceState( null, title, options.path );
+       } else {
+               history.pushState( null, title, options.path );
+       }
+};
+
+/**
+ * Navigate to a specific ''hash fragment'' route.
+ *
+ * @param {string} path String with a route (hash without #).
+ * @deprecated use navigateTo instead
+ */
+OO.Router.prototype.navigate = function ( path ) {
        // Take advantage of `pushState` when available, to clear the hash and
        // not leave `#` in the history. An entry with `#` in the history has
        // the side-effect of resetting the scroll position when navigating the
        // history.
-       if ( path === '' && history && history.pushState ) {
+       if ( path === '' ) {
                // To clear the hash we need to cut the hash from the URL.
                path = window.location.href.replace( /#.*$/, '' );
                history.pushState( null, document.title, path );
@@ -141,30 +177,22 @@ Router.prototype.navigate = function ( path ) {
        }
 };
 
-/**
- * Triggers back on the window
- */
-Router.prototype.goBack = function () {
-       window.history.back();
-};
-
 /**
  * Navigate to the previous route. This is a wrapper for window.history.back
  *
- * @method
- * @return {jQuery.Deferred}
+ * @return {jQuery.Promise} Promise which resolves when the back navigation is complete
  */
-Router.prototype.back = function () {
-       var deferredRequest = $.Deferred(),
-               self = this,
-               timeoutID;
+OO.Router.prototype.back = function () {
+       var timeoutID,
+               router = this,
+               deferred = $.Deferred();
 
        this.once( 'popstate', function () {
                clearTimeout( timeoutID );
-               deferredRequest.resolve();
+               deferred.resolve();
        } );
 
-       this.goBack();
+       window.history.back();
 
        // If for some reason (old browser, bug in IE/windows 8.1, etc) popstate doesn't fire,
        // resolve manually. Since we don't know for sure which browsers besides IE10/11 have
@@ -173,33 +201,29 @@ Router.prototype.back = function () {
        // See https://connect.microsoft.com/IE/feedback/details/793618/history-back-popstate-not-working-as-expected-in-webview-control
        // Give browser a few ms to update its history.
        timeoutID = setTimeout( function () {
-               self.off( 'popstate' );
-               deferredRequest.resolve();
+               router.off( 'popstate' );
+               deferred.resolve();
        }, 50 );
 
-       return deferredRequest;
+       return deferred.promise();
 };
 
 /**
  * Get current path (hash).
  *
- * @method
  * @return {string} Current path.
  */
-Router.prototype.getPath = function () {
+OO.Router.prototype.getPath = function () {
        return window.location.hash.slice( 1 );
 };
 
 /**
- * Determine if current browser supports onhashchange event
- *
- * @method
- * @return {boolean}
+ * @deprecated Use static method
  */
-Router.prototype.isSupported = function () {
-       return 'onhashchange' in window;
-};
+OO.Router.prototype.isSupported = OO.Router.static.isSupported;
 
-module.exports = Router;
+if ( typeof module !== 'undefined' && module.exports ) {
+       module.exports = OO.Router;
+}
 
 }( jQuery ) );
diff --git a/resources/src/jquery.tablesorter.styles/images/sort_both.png b/resources/src/jquery.tablesorter.styles/images/sort_both.png
new file mode 100644 (file)
index 0000000..fc63091
Binary files /dev/null and b/resources/src/jquery.tablesorter.styles/images/sort_both.png differ
diff --git a/resources/src/jquery.tablesorter.styles/images/sort_both.svg b/resources/src/jquery.tablesorter.styles/images/sort_both.svg
new file mode 100644 (file)
index 0000000..e145c1b
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="21" height="9" viewBox="0 0 21 9">
+       <path d="M14.5 5l-4 4-4-4zm0-1l-4-4-4 4z"/>
+</svg>
diff --git a/resources/src/jquery.tablesorter.styles/images/sort_down.png b/resources/src/jquery.tablesorter.styles/images/sort_down.png
new file mode 100644 (file)
index 0000000..ce04a0f
Binary files /dev/null and b/resources/src/jquery.tablesorter.styles/images/sort_down.png differ
diff --git a/resources/src/jquery.tablesorter.styles/images/sort_down.svg b/resources/src/jquery.tablesorter.styles/images/sort_down.svg
new file mode 100644 (file)
index 0000000..452606a
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="21" height="4" viewBox="0 0 21 4">
+       <path d="M14.5 0l-4 4-4-4z"/>
+</svg>
diff --git a/resources/src/jquery.tablesorter.styles/images/sort_up.png b/resources/src/jquery.tablesorter.styles/images/sort_up.png
new file mode 100644 (file)
index 0000000..2ebe071
Binary files /dev/null and b/resources/src/jquery.tablesorter.styles/images/sort_up.png differ
diff --git a/resources/src/jquery.tablesorter.styles/images/sort_up.svg b/resources/src/jquery.tablesorter.styles/images/sort_up.svg
new file mode 100644 (file)
index 0000000..38f6374
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="21" height="4" viewBox="0 0 21 4">
+       <path d="M6.5 4l4-4 4 4z"/>
+</svg>
diff --git a/resources/src/jquery.tablesorter.styles/jquery.tablesorter.styles.less b/resources/src/jquery.tablesorter.styles/jquery.tablesorter.styles.less
new file mode 100644 (file)
index 0000000..c941da0
--- /dev/null
@@ -0,0 +1,22 @@
+@import 'mediawiki.mixins';
+
+/* Table Sorting */
+
+.client-js .sortable:not( .jquery-tablesorter ) > thead > :last-of-type > th:not( .unsortable ),
+.jquery-tablesorter th.headerSort {
+       .background-image-svg( 'images/sort_both.svg', 'images/sort_both.png' );
+       cursor: pointer;
+       background-repeat: no-repeat;
+       background-position: center right;
+       padding-right: 21px;
+}
+
+.jquery-tablesorter {
+       th.headerSortUp {
+               .background-image-svg( 'images/sort_up.svg', 'images/sort_up.png' );
+       }
+
+       th.headerSortDown {
+               .background-image-svg( 'images/sort_down.svg', 'images/sort_down.png' );
+       }
+}
diff --git a/resources/src/jquery.tablesorter/images/sort_both.png b/resources/src/jquery.tablesorter/images/sort_both.png
deleted file mode 100644 (file)
index fc63091..0000000
Binary files a/resources/src/jquery.tablesorter/images/sort_both.png and /dev/null differ
diff --git a/resources/src/jquery.tablesorter/images/sort_both.svg b/resources/src/jquery.tablesorter/images/sort_both.svg
deleted file mode 100644 (file)
index 872a8db..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="21" height="9" viewBox="0 0 21 9">
-       <path d="M14.5 5l-4 4-4-4zM14.5 4l-4-4-4 4z"/>
-</svg>
diff --git a/resources/src/jquery.tablesorter/images/sort_down.png b/resources/src/jquery.tablesorter/images/sort_down.png
deleted file mode 100644 (file)
index ce04a0f..0000000
Binary files a/resources/src/jquery.tablesorter/images/sort_down.png and /dev/null differ
diff --git a/resources/src/jquery.tablesorter/images/sort_down.svg b/resources/src/jquery.tablesorter/images/sort_down.svg
deleted file mode 100644 (file)
index 452606a..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="21" height="4" viewBox="0 0 21 4">
-       <path d="M14.5 0l-4 4-4-4z"/>
-</svg>
diff --git a/resources/src/jquery.tablesorter/images/sort_up.png b/resources/src/jquery.tablesorter/images/sort_up.png
deleted file mode 100644 (file)
index 2ebe071..0000000
Binary files a/resources/src/jquery.tablesorter/images/sort_up.png and /dev/null differ
diff --git a/resources/src/jquery.tablesorter/images/sort_up.svg b/resources/src/jquery.tablesorter/images/sort_up.svg
deleted file mode 100644 (file)
index 38f6374..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="21" height="4" viewBox="0 0 21 4">
-       <path d="M6.5 4l4-4 4 4z"/>
-</svg>
diff --git a/resources/src/jquery.tablesorter/jquery.tablesorter.less b/resources/src/jquery.tablesorter/jquery.tablesorter.less
deleted file mode 100644 (file)
index ce24b0d..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-@import 'mediawiki.mixins';
-
-/* Table Sorting */
-
-table.jquery-tablesorter {
-       th.headerSort {
-               .background-image-svg( 'images/sort_both.svg', 'images/sort_both.png' );
-               cursor: pointer;
-               background-repeat: no-repeat;
-               background-position: center right;
-               padding-right: 21px;
-       }
-
-       th.headerSortUp {
-               .background-image-svg( 'images/sort_up.svg', 'images/sort_up.png' );
-       }
-
-       th.headerSortDown {
-               .background-image-svg( 'images/sort_down.svg', 'images/sort_down.png' );
-       }
-}
index f5f0475..09306f6 100644 (file)
@@ -82,7 +82,6 @@
                        }
                }
 
-               // eslint-disable-next-line jquery/no-animate-toggle
                $containers.toggle( action === 'expand' );
                hookCallback();
        }
index 01d2ba5..d9a094c 100644 (file)
                                // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden
                                // if the textbox is empty then clear the result div, but leave other settings intouched
                                if ( val.length === 0 ) {
-                                       // eslint-disable-next-line jquery/no-animate-toggle
                                        $.suggestions.hide( context );
                                        context.data.prevText = '';
                                } else if (
                                        if ( context.data !== undefined ) {
                                                if ( context.data.$textbox.val().length === 0 ) {
                                                        // Hide the div when no suggestion exist
-                                                       // eslint-disable-next-line jquery/no-animate-toggle
                                                        $.suggestions.hide( context );
                                                } else {
                                                        // Rebuild the suggestions list
                                        break;
                                // Escape
                                case 27:
-                                       // eslint-disable-next-line jquery/no-animate-toggle
                                        $.suggestions.hide( context );
                                        $.suggestions.restore( context );
                                        $.suggestions.cancel( context );
                                case 13:
                                        preventDefault = wasVisible;
                                        selected = context.data.$container.find( '.suggestions-result-current' );
-                                       // eslint-disable-next-line jquery/no-animate-toggle
                                        $.suggestions.hide( context );
                                        if ( selected.length === 0 || context.data.selectedWithMouse ) {
                                                // If nothing is selected or if something was selected with the mouse
                                                                        // This will hide the link we're just clicking on, which causes problems
                                                                        // when done synchronously in at least Firefox 3.6 (T64858).
                                                                        setTimeout( function () {
-                                                                               // eslint-disable-next-line jquery/no-animate-toggle
                                                                                $.suggestions.hide( context );
                                                                        } );
                                                                }
                                                                        // This will hide the link we're just clicking on, which causes problems
                                                                        // when done synchronously in at least Firefox 3.6 (T64858).
                                                                        setTimeout( function () {
-                                                                               // eslint-disable-next-line jquery/no-animate-toggle
                                                                                $.suggestions.hide( context );
                                                                        } );
                                                                }
                                        } )
                                        .on( 'keypress', function ( e ) {
                                                context.data.keypressedCount++;
-                                               // eslint-disable-next-line jquery/no-event-shorthand
                                                $.suggestions.keypress( e, context, context.data.keypressed );
                                        } )
                                        .on( 'keyup', function ( e ) {
                                                        e.which === context.data.keypressed &&
                                                        allowed.indexOf( e.which ) !== -1
                                                ) {
-                                                       // eslint-disable-next-line jquery/no-event-shorthand
                                                        $.suggestions.keypress( e, context, context.data.keypressed );
                                                }
                                        } )
                                                if ( context.data.mouseDownOn.length > 0 ) {
                                                        return;
                                                }
-                                               // eslint-disable-next-line jquery/no-animate-toggle
                                                $.suggestions.hide( context );
                                                $.suggestions.cancel( context );
                                        } );
index 9974e2b..24806b5 100644 (file)
                        fileReader = new FileReader();
                        fileReader.onload = function () {
                                var fileStr, arr, i, metadata,
-                                       jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
+                                       jpegmeta = require( 'mediawiki.libs.jpegmeta' );
 
                                if ( typeof fileReader.result === 'string' ) {
                                        fileStr = fileReader.result;
index 2a167fe..619cf38 100644 (file)
@@ -25,7 +25,7 @@
 
                // Config for uploads to local wiki.
                // Can be overridden with foreign wiki config when #loadConfig is called.
-               this.config = mw.config.get( 'wgUploadDialog' );
+               this.config = require( './config.json' ).UploadDialog;
 
                mw.ForeignUpload.call( this, target, apiconfig );
        }
index c903828..1c306ad 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="42" height="20" viewBox="0 0 42 20">
-       <path fill="#fff" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M11 10h17.064"/>
+       <path fill="#fff" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M11 10h17.064"/>
        <path d="M23 6l8 4-8 4z"/>
 </svg>
index c037339..23c7769 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="42" height="20" viewBox="0 0 42 20">
-       <path fill="#fff" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M31 10H13.936"/>
+       <path fill="#fff" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M31 10H13.936"/>
        <path d="M19 6l-8 4 8 4z"/>
 </svg>
index 46475ef..49c4d92 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="47" height="20" viewBox="0 0 47 20">
-       <path fill="none" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M14.98 2.5V11c0 1.04 1.02 1.98 2.02 1.98h6l3 .02"/>
+       <path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M14.98 2.5V11c0 1.04 1.02 1.98 2.02 1.98h6l3 .02"/>
        <path d="M23.48 9.5l.02 7L30 13z"/>
 </svg>
index e06c4d2..7d10f34 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="47" height="20" viewBox="0 0 47 20">
-       <path fill="none" stroke="#000" stroke-width="2" stroke-miterlimit="10" d="M32.002 2.5V11c0 1.04-1.02 1.98-2.02 1.98h-6l-3 .02"/>
+       <path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2" d="M32.002 2.5V11c0 1.04-1.02 1.98-2.02 1.98h-6l-3 .02"/>
        <path d="M23.502 9.5l-.02 7-6.5-3.5z"/>
 </svg>
index d38feb5..5b0cb0f 100644 (file)
@@ -15,5 +15,5 @@
        <rect width="246" height="246" x="5" y="5" fill="#f49c52" rx="50" ry="50"/>
        <rect width="236" height="236" x="10" y="10" fill="url(#a)" rx="47" ry="47"/>
        <circle cx="68" cy="189" r="24" fill="#fff"/>
-       <path fill="#fff" d="M160 213h-34a82 82 0 0 0-82-82v-34a116 116 0 0 1 116 116zM184 213a140 140 0 0 0-140-140v-35a175 175 0 0 1 175 175z"/>
+       <path fill="#fff" d="M160 213h-34a82 82 0 0 0-82-82V97a116 116 0 0 1 116 116zm24 0A140 140 0 0 0 44 73V38a175 175 0 0 1 175 175z"/>
 </svg>
index 2b1ccf9..4fc6a0c 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-       <path d="M12.001 2.085c-5.478 0-9.916 4.438-9.916 9.916 0 5.476 4.438 9.914 9.916 9.914 5.476 0 9.914-4.438 9.914-9.914 0-5.478-4.438-9.916-9.914-9.916zm.001 18c-4.465 0-8.084-3.619-8.084-8.083 0-4.465 3.619-8.084 8.084-8.084 4.464 0 8.083 3.619 8.083 8.084 0 4.464-3.619 8.083-8.083 8.083z"/>
-       <path d="M11.766 6.688c-2.5 0-3.219 2.188-3.219 2.188l1.411.854s.298-.791.901-1.229c.516-.375 1.625-.625 2.219.125.701.885-.17 1.587-1.078 2.719-.953 1.186-1 3.655-1 3.655h1.969s.135-2.318 1.041-3.381c.603-.707 1.443-1.338 1.443-2.494s-1.187-2.437-3.687-2.437zM11 16h2v2h-2z"/>
+       <path d="M12.001 2.085a9.914 9.914 0 0 0-9.916 9.916c0 5.476 4.438 9.914 9.916 9.914a9.913 9.913 0 0 0 9.914-9.914c0-5.478-4.438-9.916-9.914-9.916zm.001 18a8.083 8.083 0 1 1 0-16.167 8.083 8.083 0 0 1 0 16.167z"/>
+       <path d="M11.766 6.688c-2.5 0-3.219 2.188-3.219 2.188l1.411.854s.298-.791.901-1.229c.516-.375 1.625-.625 2.219.125.701.885-.17 1.587-1.078 2.719C11.047 12.531 11 15 11 15h1.969s.135-2.318 1.041-3.381c.603-.707 1.443-1.338 1.443-2.494s-1.187-2.437-3.687-2.437zM11 16h2v2h-2z"/>
 </svg>
index 655076f..e228ae8 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="21.059" height="21.06">
-       <path fill="#54595d" d="M10.529 0c-5.814 0-10.529 4.714-10.529 10.529s4.715 10.53 10.529 10.53c5.816 0 10.529-4.715 10.529-10.53s-4.712-10.529-10.529-10.529zm-.002 16.767c-.861 0-1.498-.688-1.498-1.516 0-.862.637-1.534 1.498-1.534.828 0 1.5.672 1.5 1.534 0 .827-.672 1.516-1.5 1.516zm2.137-6.512c-.723.568-1 .931-1 1.739v.5h-2.205v-.603c0-1.517.449-2.136 1.154-2.688.707-.552 1.139-.845 1.139-1.637 0-.672-.414-1.051-1.24-1.051-.707 0-1.328.189-1.982.638l-1.051-1.807c.861-.604 1.93-1.034 3.342-1.034 1.912 0 3.516 1.051 3.516 3.066-.001 1.43-.794 2.188-1.673 2.877z"/>
+       <path fill="#54595d" d="M10.529 0C4.715 0 0 4.714 0 10.529s4.715 10.53 10.529 10.53c5.816 0 10.529-4.715 10.529-10.53S16.346 0 10.529 0zm-.002 16.767c-.861 0-1.498-.688-1.498-1.516 0-.862.637-1.534 1.498-1.534.828 0 1.5.672 1.5 1.534 0 .827-.672 1.516-1.5 1.516zm2.137-6.512c-.723.568-1 .931-1 1.739v.5H9.459v-.603c0-1.517.449-2.136 1.154-2.688.707-.552 1.139-.845 1.139-1.637 0-.672-.414-1.051-1.24-1.051-.707 0-1.328.189-1.982.638L7.479 5.346c.861-.604 1.93-1.034 3.342-1.034 1.912 0 3.516 1.051 3.516 3.066-.001 1.43-.794 2.188-1.673 2.877z"/>
 </svg>
index 076e02b..56127fe 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12">
-       <path fill="#72777d" d="M8 1.533v9.671l-4.752-4.871z"/>
+       <path fill="#72777d" d="M8 1.533v9.671L3.248 6.333z"/>
 </svg>
index f13144d..437f4b5 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12">
-       <path fill="#72777d" d="M1.165 3.624h9.671l-4.871 4.752z"/>
+       <path fill="#72777d" d="M1.165 3.624h9.671L5.965 8.376z"/>
 </svg>
index 72a27fa..849ccbc 100644 (file)
                } );
        }
 
-       function humanSize( bytes ) {
+       function humanSize( bytesInput ) {
                var i,
+                       bytes = +bytesInput,
                        units = [ '', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB' ];
 
-               if ( !$.isNumeric( bytes ) || bytes === 0 ) {
-                       return bytes;
+               if ( bytes === 0 || isNaN( bytes ) ) {
+                       return bytesInput;
                }
 
                for ( i = 0; bytes >= 1024; bytes /= 1024 ) {
index fdc988b..846deb9 100644 (file)
        var oldParser,
                slice = Array.prototype.slice,
                parserDefaults = {
+                       // Magic words and their expansions. Server-side data is added to this below.
                        magic: {
                                PAGENAME: mw.config.get( 'wgPageName' ),
                                PAGENAMEE: mw.util.wikiUrlencode( mw.config.get( 'wgPageName' ) )
                        },
                        // Whitelist for allowed HTML elements in wikitext.
                        // Self-closing tags are not currently supported.
-                       // Can be populated via setParserDefaults().
+                       // Filled in with server-side data below
                        allowedHtmlElements: [],
                        // Key tag name, value allowed attributes for that tag.
                        // See Sanitizer::setupAttributeWhitelist
@@ -56,6 +57,9 @@
                        format: 'parse'
                };
 
+       // Add in server-side data (allowedHtmlElements and magic words)
+       $.extend( true, parserDefaults, require( './parserDefaults.json' ) );
+
        /**
         * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
         * convert what it detects as an htmlString to an element.
index 8fed695..277034b 100644 (file)
                        return text;
                },
 
-               setSpecialCharacters: function ( data ) {
-                       this.specialCharacters = data;
-               },
-
                /**
                 * Formats language tags according the BCP 47 standard.
                 * See LanguageCode::bcp47 for the PHP implementation.
diff --git a/resources/src/mediawiki.language/mediawiki.language.names.js b/resources/src/mediawiki.language/mediawiki.language.names.js
new file mode 100644 (file)
index 0000000..fa7edca
--- /dev/null
@@ -0,0 +1,4 @@
+( function () {
+       var names = require( './names.json' );
+       mw.language.setData( mw.config.get( 'wgUserLanguage' ), 'languageData', names );
+}() );
diff --git a/resources/src/mediawiki.language/mediawiki.language.specialCharacters.js b/resources/src/mediawiki.language/mediawiki.language.specialCharacters.js
new file mode 100644 (file)
index 0000000..6674adb
--- /dev/null
@@ -0,0 +1,9 @@
+( function () {
+       var specialCharacters = require( './specialcharacters.json' );
+       // Deprecated since 1.33
+       mw.log.deprecate( mw.language, 'specialCharacters', specialCharacters,
+               'Use require( \'mediawiki.language.specialCharacters\' ) instead',
+               'mw.language.specialCharacters'
+       );
+       module.exports = specialCharacters;
+}() );
index 40561e4..8a74c10 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="15" height="11" viewBox="0 0 11 15">
        <g fill="#fff" stroke="#000">
-               <path d="M1.509 1.865h10.99v7.919h-10.99z"/>
+               <path d="M1.509 1.865h10.99v7.919H1.509z"/>
                <path d="M-1.499 6.868h5.943v4.904h-5.943z"/>
        </g>
 </svg>
index 1b4cbfe..e337a54 100644 (file)
@@ -2,6 +2,6 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="15" height="11" viewBox="0 0 11 15">
        <g fill="#fff" stroke="#000">
                <path d="M9.491 1.865h-10.99v7.919h10.99z"/>
-               <path d="M12.499 6.868h-5.943v4.904h5.943z"/>
+               <path d="M12.499 6.868H6.556v4.904h5.943z"/>
        </g>
 </svg>
index 0d1a657..6a75db0 100644 (file)
@@ -63,7 +63,7 @@
 @colorButtonText: @colorGray2;
 @colorButtonTextHighlight: @colorGray4;
 @colorButtonTextActive: @colorGray1;
-@colorDisabledText: @colorGray12;
+@colorDisabledText: @colorGray7;
 @colorErrorText: #d33;
 @colorWarningText: #705000;
 
index a28bd8f..849e8f2 100644 (file)
@@ -1,12 +1,6 @@
 /* global JpegMeta */
-( function () {
 
-       // Export as module
-       module.exports = function ( fileReaderResult, fileName ) {
-               return new JpegMeta.JpegFile( fileReaderResult, fileName );
-       };
-
-       // Back-compat: Also expose via mw.lib
-       // @deprecated since 1.31
-       mw.log.deprecate( mw.libs, 'jpegmeta', module.exports );
-}() );
+// Export as module
+module.exports = function ( fileReaderResult, fileName ) {
+       return new JpegMeta.JpegFile( fileReaderResult, fileName );
+};
index 2765d07..e8450df 100644 (file)
                                        $area.css( 'display', 'none' );
                                        notif.$notification.remove();
                                } else {
+                                       // FIXME: Use CSS transition
+                                       // eslint-disable-next-line jquery/no-slide
                                        notif.$notification.slideUp( 'fast', function () {
                                                $( this ).remove();
                                        } );
index cd7990e..7fdec90 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12">
-       <path fill="#36c" d="M1 10h10l-5-8.658z"/>
+       <path fill="#36c" d="M1 10h10L6 1.342z"/>
 </svg>
diff --git a/resources/src/mediawiki.rcfilters/Controller.js b/resources/src/mediawiki.rcfilters/Controller.js
new file mode 100644 (file)
index 0000000..56a95eb
--- /dev/null
@@ -0,0 +1,1230 @@
+( function () {
+
+       var byteLength = require( 'mediawiki.String' ).byteLength,
+               UriProcessor = require( './UriProcessor.js' ),
+               Controller;
+
+       /* eslint no-underscore-dangle: "off" */
+       /**
+        * Controller for the filters in Recent Changes
+        * @class mw.rcfilters.Controller
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+        * @param {Object} config Additional configuration
+        * @cfg {string} savedQueriesPreferenceName Where to save the saved queries
+        * @cfg {string} daysPreferenceName Preference name for the days filter
+        * @cfg {string} limitPreferenceName Preference name for the limit filter
+        * @cfg {string} collapsedPreferenceName Preference name for collapsing and showing
+        *  the active filters area
+        * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
+        *  title normalization to separate title subpage/parts into the target= url
+        *  parameter
+        */
+       Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel, config ) {
+               this.filtersModel = filtersModel;
+               this.changesListModel = changesListModel;
+               this.savedQueriesModel = savedQueriesModel;
+               this.savedQueriesPreferenceName = config.savedQueriesPreferenceName;
+               this.daysPreferenceName = config.daysPreferenceName;
+               this.limitPreferenceName = config.limitPreferenceName;
+               this.collapsedPreferenceName = config.collapsedPreferenceName;
+               this.normalizeTarget = !!config.normalizeTarget;
+
+               this.pollingRate = require( './config.json' ).StructuredChangeFiltersLiveUpdatePollingRate;
+
+               this.requestCounter = {};
+               this.baseFilterState = {};
+               this.uriProcessor = null;
+               this.initialized = false;
+               this.wereSavedQueriesSaved = false;
+
+               this.prevLoggedItems = [];
+
+               this.FILTER_CHANGE = 'filterChange';
+               this.SHOW_NEW_CHANGES = 'showNewChanges';
+               this.LIVE_UPDATE = 'liveUpdate';
+       };
+
+       /* Initialization */
+       OO.initClass( Controller );
+
+       /**
+        * Initialize the filter and parameter states
+        *
+        * @param {Array} filterStructure Filter definition and structure for the model
+        * @param {Object} [namespaceStructure] Namespace definition
+        * @param {Object} [tagList] Tag definition
+        * @param {Object} [conditionalViews] Conditional view definition
+        */
+       Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList, conditionalViews ) {
+               var parsedSavedQueries, pieces,
+                       displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
+                       defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
+                       controller = this,
+                       views = $.extend( true, {}, conditionalViews ),
+                       items = [],
+                       uri = new mw.Uri();
+
+               // Prepare views
+               if ( namespaceStructure ) {
+                       items = [];
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( namespaceStructure, function ( namespaceID, label ) {
+                               // Build and clean up the individual namespace items definition
+                               items.push( {
+                                       name: namespaceID,
+                                       label: label || mw.msg( 'blanknamespace' ),
+                                       description: '',
+                                       identifiers: [
+                                               mw.Title.isTalkNamespace( namespaceID ) ?
+                                                       'talk' : 'subject'
+                                       ],
+                                       cssClass: 'mw-changeslist-ns-' + namespaceID
+                               } );
+                       } );
+
+                       views.namespaces = {
+                               title: mw.msg( 'namespaces' ),
+                               trigger: ':',
+                               groups: [ {
+                                       // Group definition (single group)
+                                       name: 'namespace', // parameter name is singular
+                                       type: 'string_options',
+                                       title: mw.msg( 'namespaces' ),
+                                       labelPrefixKey: { default: 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+                                       separator: ';',
+                                       fullCoverage: true,
+                                       filters: items
+                               } ]
+                       };
+                       views.invert = {
+                               groups: [
+                                       {
+                                               name: 'invertGroup',
+                                               type: 'boolean',
+                                               hidden: true,
+                                               filters: [ {
+                                                       name: 'invert',
+                                                       default: '0'
+                                               } ]
+                                       } ]
+                       };
+               }
+               if ( tagList ) {
+                       views.tags = {
+                               title: mw.msg( 'rcfilters-view-tags' ),
+                               trigger: '#',
+                               groups: [ {
+                                       // Group definition (single group)
+                                       name: 'tagfilter', // Parameter name
+                                       type: 'string_options',
+                                       title: 'rcfilters-view-tags', // Message key
+                                       labelPrefixKey: 'rcfilters-tag-prefix-tags',
+                                       separator: '|',
+                                       fullCoverage: false,
+                                       filters: tagList
+                               } ]
+                       };
+               }
+
+               // Add parameter range operations
+               views.range = {
+                       groups: [
+                               {
+                                       name: 'limit',
+                                       type: 'single_option',
+                                       title: '', // Because it's a hidden group, this title actually appears nowhere
+                                       hidden: true,
+                                       allowArbitrary: true,
+                                       // FIXME: $.isNumeric is deprecated
+                                       validate: $.isNumeric,
+                                       range: {
+                                               min: 0, // The server normalizes negative numbers to 0 results
+                                               max: 1000
+                                       },
+                                       sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+                                       default: mw.user.options.get( this.limitPreferenceName, displayConfig.limitDefault ),
+                                       sticky: true,
+                                       filters: displayConfig.limitArray.map( function ( num ) {
+                                               return controller._createFilterDataFromNumber( num, num );
+                                       } )
+                               },
+                               {
+                                       name: 'days',
+                                       type: 'single_option',
+                                       title: '', // Because it's a hidden group, this title actually appears nowhere
+                                       hidden: true,
+                                       allowArbitrary: true,
+                                       // FIXME: $.isNumeric is deprecated
+                                       validate: $.isNumeric,
+                                       range: {
+                                               min: 0,
+                                               max: displayConfig.maxDays
+                                       },
+                                       sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+                                       numToLabelFunc: function ( i ) {
+                                               return Number( i ) < 1 ?
+                                                       ( Number( i ) * 24 ).toFixed( 2 ) :
+                                                       Number( i );
+                                       },
+                                       default: mw.user.options.get( this.daysPreferenceName, displayConfig.daysDefault ),
+                                       sticky: true,
+                                       filters: [
+                                               // Hours (1, 2, 6, 12)
+                                               0.04166, 0.0833, 0.25, 0.5
+                                       // Days
+                                       ].concat( displayConfig.daysArray )
+                                               .map( function ( num ) {
+                                                       return controller._createFilterDataFromNumber(
+                                                               num,
+                                                               // Convert fractions of days to number of hours for the labels
+                                                               num < 1 ? Math.round( num * 24 ) : num
+                                                       );
+                                               } )
+                               }
+                       ]
+               };
+
+               views.display = {
+                       groups: [
+                               {
+                                       name: 'display',
+                                       type: 'boolean',
+                                       title: '', // Because it's a hidden group, this title actually appears nowhere
+                                       hidden: true,
+                                       sticky: true,
+                                       filters: [
+                                               {
+                                                       name: 'enhanced',
+                                                       default: String( mw.user.options.get( 'usenewrc', 0 ) )
+                                               }
+                                       ]
+                               }
+                       ]
+               };
+
+               // Before we do anything, we need to see if we require additional items in the
+               // groups that have 'AllowArbitrary'. For the moment, those are only single_option
+               // groups; if we ever expand it, this might need further generalization:
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( views, function ( viewName, viewData ) {
+                       viewData.groups.forEach( function ( groupData ) {
+                               var extraValues = [];
+                               if ( groupData.allowArbitrary ) {
+                                       // If the value in the URI isn't in the group, add it
+                                       if ( uri.query[ groupData.name ] !== undefined ) {
+                                               extraValues.push( uri.query[ groupData.name ] );
+                                       }
+                                       // If the default value isn't in the group, add it
+                                       if ( groupData.default !== undefined ) {
+                                               extraValues.push( String( groupData.default ) );
+                                       }
+                                       controller.addNumberValuesToGroup( groupData, extraValues );
+                               }
+                       } );
+               } );
+
+               // Initialize the model
+               this.filtersModel.initializeFilters( filterStructure, views );
+
+               this.uriProcessor = new UriProcessor(
+                       this.filtersModel,
+                       { normalizeTarget: this.normalizeTarget }
+               );
+
+               if ( !mw.user.isAnon() ) {
+                       try {
+                               parsedSavedQueries = JSON.parse( mw.user.options.get( this.savedQueriesPreferenceName ) || '{}' );
+                       } catch ( err ) {
+                               parsedSavedQueries = {};
+                       }
+
+                       // Initialize saved queries
+                       this.savedQueriesModel.initialize( parsedSavedQueries );
+                       if ( this.savedQueriesModel.isConverted() ) {
+                               // Since we know we converted, we're going to re-save
+                               // the queries so they are now migrated to the new format
+                               this._saveSavedQueries();
+                       }
+               }
+
+               if ( defaultSavedQueryExists ) {
+                       // This came from the server, meaning that we have a default
+                       // saved query, but the server could not load it, probably because
+                       // it was pre-conversion to the new format.
+                       // We need to load this query again
+                       this.applySavedQuery( this.savedQueriesModel.getDefault() );
+               } else {
+                       // There are either recognized parameters in the URL
+                       // or there are none, but there is also no default
+                       // saved query (so defaults are from the backend)
+                       // We want to update the state but not fetch results
+                       // again
+                       this.updateStateFromUrl( false );
+
+                       pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
+
+                       // Update the changes list with the existing data
+                       // so it gets processed
+                       this.changesListModel.update(
+                               pieces.changes,
+                               pieces.fieldset,
+                               pieces.noResultsDetails,
+                               true // We're using existing DOM elements
+                       );
+               }
+
+               this.initialized = true;
+               this.switchView( 'default' );
+
+               if ( this.pollingRate ) {
+                       this._scheduleLiveUpdate();
+               }
+       };
+
+       /**
+        * Check if the controller has finished initializing.
+        * @return {boolean} Controller is initialized
+        */
+       Controller.prototype.isInitialized = function () {
+               return this.initialized;
+       };
+
+       /**
+        * Extracts information from the changes list DOM
+        *
+        * @param {jQuery} $root Root DOM to find children from
+        * @param {boolean} [statusCode] Server response status code
+        * @return {Object} Information about changes list
+        * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
+        *   (either normally or as an error)
+        * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
+        *   'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
+        * @return {jQuery} return.fieldset Fieldset
+        */
+       Controller.prototype._extractChangesListInfo = function ( $root, statusCode ) {
+               var info,
+                       $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
+                       areResults = !!$changesListContents.length,
+                       checkForLogout = !areResults && statusCode === 200;
+
+               // We check if user logged out on different tab/browser or the session has expired.
+               // 205 status code returned from the server, which indicates that we need to reload the page
+               // is not usable on WL page, because we get redirected to login page, which gives 200 OK
+               // status code (if everything else goes well).
+               // Bug: T177717
+               if ( checkForLogout && !!$root.find( '#wpName1' ).length ) {
+                       location.reload( false );
+                       return;
+               }
+
+               info = {
+                       changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
+                       fieldset: $root.find( 'fieldset.cloptions' ).first()
+               };
+
+               if ( !areResults ) {
+                       if ( $root.find( '.mw-changeslist-timeout' ).length ) {
+                               info.noResultsDetails = 'NO_RESULTS_TIMEOUT';
+                       } else if ( $root.find( '.mw-changeslist-notargetpage' ).length ) {
+                               info.noResultsDetails = 'NO_RESULTS_NO_TARGET_PAGE';
+                       } else if ( $root.find( '.mw-changeslist-invalidtargetpage' ).length ) {
+                               info.noResultsDetails = 'NO_RESULTS_INVALID_TARGET_PAGE';
+                       } else {
+                               info.noResultsDetails = 'NO_RESULTS_NORMAL';
+                       }
+               }
+
+               return info;
+       };
+
+       /**
+        * Create filter data from a number, for the filters that are numerical value
+        *
+        * @param {number} num Number
+        * @param {number} numForDisplay Number for the label
+        * @return {Object} Filter data
+        */
+       Controller.prototype._createFilterDataFromNumber = function ( num, numForDisplay ) {
+               return {
+                       name: String( num ),
+                       label: mw.language.convertNumber( numForDisplay )
+               };
+       };
+
+       /**
+        * Add an arbitrary values to groups that allow arbitrary values
+        *
+        * @param {Object} groupData Group data
+        * @param {string|string[]} arbitraryValues An array of arbitrary values to add to the group
+        */
+       Controller.prototype.addNumberValuesToGroup = function ( groupData, arbitraryValues ) {
+               var controller = this,
+                       normalizeWithinRange = function ( range, val ) {
+                               if ( val < range.min ) {
+                                       return range.min; // Min
+                               } else if ( val >= range.max ) {
+                                       return range.max; // Max
+                               }
+                               return val;
+                       };
+
+               arbitraryValues = Array.isArray( arbitraryValues ) ? arbitraryValues : [ arbitraryValues ];
+
+               // Normalize the arbitrary values and the default value for a range
+               if ( groupData.range ) {
+                       arbitraryValues = arbitraryValues.map( function ( val ) {
+                               return normalizeWithinRange( groupData.range, val );
+                       } );
+
+                       // Normalize the default, since that's user defined
+                       if ( groupData.default !== undefined ) {
+                               groupData.default = String( normalizeWithinRange( groupData.range, groupData.default ) );
+                       }
+               }
+
+               // This is only true for single_option group
+               // We assume these are the only groups that will allow for
+               // arbitrary, since it doesn't make any sense for the other
+               // groups.
+               arbitraryValues.forEach( function ( val ) {
+                       if (
+                               // If the group allows for arbitrary data
+                               groupData.allowArbitrary &&
+                               // and it is single_option (or string_options, but we
+                               // don't have cases of those yet, nor do we plan to)
+                               groupData.type === 'single_option' &&
+                               // and, if there is a validate method and it passes on
+                               // the data
+                               ( !groupData.validate || groupData.validate( val ) ) &&
+                               // but if that value isn't already in the definition
+                               groupData.filters
+                                       .map( function ( filterData ) {
+                                               return String( filterData.name );
+                                       } )
+                                       .indexOf( String( val ) ) === -1
+                       ) {
+                               // Add the filter information
+                               groupData.filters.push( controller._createFilterDataFromNumber(
+                                       val,
+                                       groupData.numToLabelFunc ?
+                                               groupData.numToLabelFunc( val ) :
+                                               val
+                               ) );
+
+                               // If there's a sort function set up, re-sort the values
+                               if ( groupData.sortFunc ) {
+                                       groupData.filters.sort( groupData.sortFunc );
+                               }
+                       }
+               } );
+       };
+
+       /**
+        * Reset to default filters
+        */
+       Controller.prototype.resetToDefaults = function () {
+               var params = this._getDefaultParams();
+               if ( this.applyParamChange( params ) ) {
+                       // Only update the changes list if there was a change to actual filters
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL( params );
+               }
+       };
+
+       /**
+        * Check whether the default values of the filters are all false.
+        *
+        * @return {boolean} Defaults are all false
+        */
+       Controller.prototype.areDefaultsEmpty = function () {
+               return $.isEmptyObject( this._getDefaultParams() );
+       };
+
+       /**
+        * Empty all selected filters
+        */
+       Controller.prototype.emptyFilters = function () {
+               var highlightedFilterNames = this.filtersModel.getHighlightedItems()
+                       .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
+
+               if ( this.applyParamChange( {} ) ) {
+                       // Only update the changes list if there was a change to actual filters
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL();
+               }
+
+               if ( highlightedFilterNames ) {
+                       this._trackHighlight( 'clearAll', highlightedFilterNames );
+               }
+       };
+
+       /**
+        * Update the selected state of a filter
+        *
+        * @param {string} filterName Filter name
+        * @param {boolean} [isSelected] Filter selected state
+        */
+       Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
+               var filterItem = this.filtersModel.getItemByName( filterName );
+
+               if ( !filterItem ) {
+                       // If no filter was found, break
+                       return;
+               }
+
+               isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
+
+               if ( filterItem.isSelected() !== isSelected ) {
+                       this.filtersModel.toggleFilterSelected( filterName, isSelected );
+
+                       this.updateChangesList();
+
+                       // Check filter interactions
+                       this.filtersModel.reassessFilterInteractions( filterItem );
+               }
+       };
+
+       /**
+        * Clear both highlight and selection of a filter
+        *
+        * @param {string} filterName Name of the filter item
+        */
+       Controller.prototype.clearFilter = function ( filterName ) {
+               var filterItem = this.filtersModel.getItemByName( filterName ),
+                       isHighlighted = filterItem.isHighlighted(),
+                       isSelected = filterItem.isSelected();
+
+               if ( isSelected || isHighlighted ) {
+                       this.filtersModel.clearHighlightColor( filterName );
+                       this.filtersModel.toggleFilterSelected( filterName, false );
+
+                       if ( isSelected ) {
+                               // Only update the changes list if the filter changed
+                               // its selection state. If it only changed its highlight
+                               // then don't reload
+                               this.updateChangesList();
+                       }
+
+                       this.filtersModel.reassessFilterInteractions( filterItem );
+
+                       // Log filter grouping
+                       this.trackFilterGroupings( 'removefilter' );
+               }
+
+               if ( isHighlighted ) {
+                       this._trackHighlight( 'clear', filterName );
+               }
+       };
+
+       /**
+        * Toggle the highlight feature on and off
+        */
+       Controller.prototype.toggleHighlight = function () {
+               this.filtersModel.toggleHighlight();
+               this.uriProcessor.updateURL();
+
+               if ( this.filtersModel.isHighlightEnabled() ) {
+                       mw.hook( 'RcFilters.highlight.enable' ).fire();
+               }
+       };
+
+       /**
+        * Toggle the namespaces inverted feature on and off
+        */
+       Controller.prototype.toggleInvertedNamespaces = function () {
+               this.filtersModel.toggleInvertedNamespaces();
+               if (
+                       this.filtersModel.getFiltersByView( 'namespaces' ).filter(
+                               function ( filterItem ) { return filterItem.isSelected(); }
+                       ).length
+               ) {
+                       // Only re-fetch results if there are namespace items that are actually selected
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL();
+               }
+       };
+
+       /**
+        * Set the value of the 'showlinkedto' parameter
+        * @param {boolean} value
+        */
+       Controller.prototype.setShowLinkedTo = function ( value ) {
+               var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' ),
+                       showLinkedToItem = this.filtersModel.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' );
+
+               this.filtersModel.toggleFilterSelected( showLinkedToItem.getName(), value );
+               this.uriProcessor.updateURL();
+               // reload the results only when target is set
+               if ( targetItem.getValue() ) {
+                       this.updateChangesList();
+               }
+       };
+
+       /**
+        * Set the target page
+        * @param {string} page
+        */
+       Controller.prototype.setTargetPage = function ( page ) {
+               var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' );
+               targetItem.setValue( page );
+               this.uriProcessor.updateURL();
+               this.updateChangesList();
+       };
+
+       /**
+        * Set the highlight color for a filter item
+        *
+        * @param {string} filterName Name of the filter item
+        * @param {string} color Selected color
+        */
+       Controller.prototype.setHighlightColor = function ( filterName, color ) {
+               this.filtersModel.setHighlightColor( filterName, color );
+               this.uriProcessor.updateURL();
+               this._trackHighlight( 'set', { name: filterName, color: color } );
+       };
+
+       /**
+        * Clear highlight for a filter item
+        *
+        * @param {string} filterName Name of the filter item
+        */
+       Controller.prototype.clearHighlightColor = function ( filterName ) {
+               this.filtersModel.clearHighlightColor( filterName );
+               this.uriProcessor.updateURL();
+               this._trackHighlight( 'clear', filterName );
+       };
+
+       /**
+        * Enable or disable live updates.
+        * @param {boolean} enable True to enable, false to disable
+        */
+       Controller.prototype.toggleLiveUpdate = function ( enable ) {
+               this.changesListModel.toggleLiveUpdate( enable );
+               if ( this.changesListModel.getLiveUpdate() && this.changesListModel.getNewChangesExist() ) {
+                       this.updateChangesList( null, this.LIVE_UPDATE );
+               }
+       };
+
+       /**
+        * Set a timeout for the next live update.
+        * @private
+        */
+       Controller.prototype._scheduleLiveUpdate = function () {
+               setTimeout( this._doLiveUpdate.bind( this ), this.pollingRate * 1000 );
+       };
+
+       /**
+        * Perform a live update.
+        * @private
+        */
+       Controller.prototype._doLiveUpdate = function () {
+               if ( !this._shouldCheckForNewChanges() ) {
+                       // skip this turn and check back later
+                       this._scheduleLiveUpdate();
+                       return;
+               }
+
+               this._checkForNewChanges()
+                       .then( function ( statusCode ) {
+                               // no result is 204 with the 'peek' param
+                               // logged out is 205
+                               var newChanges = statusCode === 200;
+
+                               if ( !this._shouldCheckForNewChanges() ) {
+                                       // by the time the response is received,
+                                       // it may not be appropriate anymore
+                                       return;
+                               }
+
+                               // 205 is the status code returned from server when user's logged in/out
+                               // status is not matching while fetching live update changes.
+                               // This works only on Recent Changes page. For WL, look _extractChangesListInfo.
+                               // Bug: T177717
+                               if ( statusCode === 205 ) {
+                                       location.reload( false );
+                                       return;
+                               }
+
+                               if ( newChanges ) {
+                                       if ( this.changesListModel.getLiveUpdate() ) {
+                                               return this.updateChangesList( null, this.LIVE_UPDATE );
+                                       } else {
+                                               this.changesListModel.setNewChangesExist( true );
+                                       }
+                               }
+                       }.bind( this ) )
+                       .always( this._scheduleLiveUpdate.bind( this ) );
+       };
+
+       /**
+        * @return {boolean} It's appropriate to check for new changes now
+        * @private
+        */
+       Controller.prototype._shouldCheckForNewChanges = function () {
+               return !document.hidden &&
+                       !this.filtersModel.hasConflict() &&
+                       !this.changesListModel.getNewChangesExist() &&
+                       !this.updatingChangesList &&
+                       this.changesListModel.getNextFrom();
+       };
+
+       /**
+        * Check if new changes, newer than those currently shown, are available
+        *
+        * @return {jQuery.Promise} Promise object that resolves with a bool
+        *   specifying if there are new changes or not
+        *
+        * @private
+        */
+       Controller.prototype._checkForNewChanges = function () {
+               var params = {
+                       limit: 1,
+                       peek: 1, // bypasses ChangesList specific UI
+                       from: this.changesListModel.getNextFrom(),
+                       isAnon: mw.user.isAnon()
+               };
+               return this._queryChangesList( 'liveUpdate', params ).then(
+                       function ( data ) {
+                               return data.status;
+                       }
+               );
+       };
+
+       /**
+        * Show the new changes
+        *
+        * @return {jQuery.Promise} Promise object that resolves after
+        * fetching and showing the new changes
+        */
+       Controller.prototype.showNewChanges = function () {
+               return this.updateChangesList( null, this.SHOW_NEW_CHANGES );
+       };
+
+       /**
+        * Save the current model state as a saved query
+        *
+        * @param {string} [label] Label of the saved query
+        * @param {boolean} [setAsDefault=false] This query should be set as the default
+        */
+       Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
+               // Add item
+               this.savedQueriesModel.addNewQuery(
+                       label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
+                       this.filtersModel.getCurrentParameterState( true ),
+                       setAsDefault
+               );
+
+               // Save item
+               this._saveSavedQueries();
+       };
+
+       /**
+        * Remove a saved query
+        *
+        * @param {string} queryID Query id
+        */
+       Controller.prototype.removeSavedQuery = function ( queryID ) {
+               this.savedQueriesModel.removeQuery( queryID );
+
+               this._saveSavedQueries();
+       };
+
+       /**
+        * Rename a saved query
+        *
+        * @param {string} queryID Query id
+        * @param {string} newLabel New label for the query
+        */
+       Controller.prototype.renameSavedQuery = function ( queryID, newLabel ) {
+               var queryItem = this.savedQueriesModel.getItemByID( queryID );
+
+               if ( queryItem ) {
+                       queryItem.updateLabel( newLabel );
+               }
+               this._saveSavedQueries();
+       };
+
+       /**
+        * Set a saved query as default
+        *
+        * @param {string} queryID Query Id. If null is given, default
+        *  query is reset.
+        */
+       Controller.prototype.setDefaultSavedQuery = function ( queryID ) {
+               this.savedQueriesModel.setDefault( queryID );
+               this._saveSavedQueries();
+       };
+
+       /**
+        * Load a saved query
+        *
+        * @param {string} queryID Query id
+        */
+       Controller.prototype.applySavedQuery = function ( queryID ) {
+               var currentMatchingQuery,
+                       params = this.savedQueriesModel.getItemParams( queryID );
+
+               currentMatchingQuery = this.findQueryMatchingCurrentState();
+
+               if (
+                       currentMatchingQuery &&
+                       currentMatchingQuery.getID() === queryID
+               ) {
+                       // If the query we want to load is the one that is already
+                       // loaded, don't reload it
+                       return;
+               }
+
+               if ( this.applyParamChange( params ) ) {
+                       // Update changes list only if there was a difference in filter selection
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL( params );
+               }
+
+               // Log filter grouping
+               this.trackFilterGroupings( 'savedfilters' );
+       };
+
+       /**
+        * Check whether the current filter and highlight state exists
+        * in the saved queries model.
+        *
+        * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
+        */
+       Controller.prototype.findQueryMatchingCurrentState = function () {
+               return this.savedQueriesModel.findMatchingQuery(
+                       this.filtersModel.getCurrentParameterState( true )
+               );
+       };
+
+       /**
+        * Save the current state of the saved queries model with all
+        * query item representation in the user settings.
+        */
+       Controller.prototype._saveSavedQueries = function () {
+               var stringified, oldPrefValue,
+                       backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
+                       state = this.savedQueriesModel.getState();
+
+               // Stringify state
+               stringified = JSON.stringify( state );
+
+               if ( byteLength( stringified ) > 65535 ) {
+                       // Sanity check, since the preference can only hold that.
+                       return;
+               }
+
+               if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
+                       // The queries were converted from the previous version
+                       // Keep the old string in the [prefname]-versionbackup
+                       oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
+
+                       // Save the old preference in the backup preference
+                       new mw.Api().saveOption( backupPrefName, oldPrefValue );
+                       // Update the preference for this session
+                       mw.user.options.set( backupPrefName, oldPrefValue );
+               }
+
+               // Save the preference
+               new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
+               // Update the preference for this session
+               mw.user.options.set( this.savedQueriesPreferenceName, stringified );
+
+               // Tag as already saved so we don't do this again
+               this.wereSavedQueriesSaved = true;
+       };
+
+       /**
+        * Update sticky preferences with current model state
+        */
+       Controller.prototype.updateStickyPreferences = function () {
+               // Update default sticky values with selected, whether they came from
+               // the initial defaults or from the URL value that is being normalized
+               this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).findSelectedItems()[ 0 ].getParamName() );
+               this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).findSelectedItems()[ 0 ].getParamName() );
+
+               // TODO: Make these automatic by having the model go over sticky
+               // items and update their default values automatically
+       };
+
+       /**
+        * Update the limit default value
+        *
+        * @param {number} newValue New value
+        */
+       Controller.prototype.updateLimitDefault = function ( newValue ) {
+               this.updateNumericPreference( this.limitPreferenceName, newValue );
+       };
+
+       /**
+        * Update the days default value
+        *
+        * @param {number} newValue New value
+        */
+       Controller.prototype.updateDaysDefault = function ( newValue ) {
+               this.updateNumericPreference( this.daysPreferenceName, newValue );
+       };
+
+       /**
+        * Update the group by page default value
+        *
+        * @param {boolean} newValue New value
+        */
+       Controller.prototype.updateGroupByPageDefault = function ( newValue ) {
+               this.updateNumericPreference( 'usenewrc', Number( newValue ) );
+       };
+
+       /**
+        * Update the collapsed state value
+        *
+        * @param {boolean} isCollapsed Filter area is collapsed
+        */
+       Controller.prototype.updateCollapsedState = function ( isCollapsed ) {
+               this.updateNumericPreference( this.collapsedPreferenceName, Number( isCollapsed ) );
+       };
+
+       /**
+        * Update a numeric preference with a new value
+        *
+        * @param {string} prefName Preference name
+        * @param {number|string} newValue New value
+        */
+       Controller.prototype.updateNumericPreference = function ( prefName, newValue ) {
+               // FIXME: $.isNumeric is deprecated
+               // eslint-disable-next-line jquery/no-is-numeric
+               if ( !$.isNumeric( newValue ) ) {
+                       return;
+               }
+
+               newValue = Number( newValue );
+
+               if ( mw.user.options.get( prefName ) !== newValue ) {
+                       // Save the preference
+                       new mw.Api().saveOption( prefName, newValue );
+                       // Update the preference for this session
+                       mw.user.options.set( prefName, newValue );
+               }
+       };
+
+       /**
+        * Synchronize the URL with the current state of the filters
+        * without adding an history entry.
+        */
+       Controller.prototype.replaceUrl = function () {
+               this.uriProcessor.updateURL();
+       };
+
+       /**
+        * Update filter state (selection and highlighting) based
+        * on current URL values.
+        *
+        * @param {boolean} [fetchChangesList=true] Fetch new results into the changes
+        *  list based on the updated model.
+        */
+       Controller.prototype.updateStateFromUrl = function ( fetchChangesList ) {
+               fetchChangesList = fetchChangesList === undefined ? true : !!fetchChangesList;
+
+               this.uriProcessor.updateModelBasedOnQuery();
+
+               // Update the sticky preferences, in case we received a value
+               // from the URL
+               this.updateStickyPreferences();
+
+               // Only update and fetch new results if it is requested
+               if ( fetchChangesList ) {
+                       this.updateChangesList();
+               }
+       };
+
+       /**
+        * Update the list of changes and notify the model
+        *
+        * @param {Object} [params] Extra parameters to add to the API call
+        * @param {string} [updateMode='filterChange'] One of 'filterChange', 'liveUpdate', 'showNewChanges', 'markSeen'
+        * @return {jQuery.Promise} Promise that is resolved when the update is complete
+        */
+       Controller.prototype.updateChangesList = function ( params, updateMode ) {
+               updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode;
+
+               if ( updateMode === this.FILTER_CHANGE ) {
+                       this.uriProcessor.updateURL( params );
+               }
+               if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) {
+                       this.changesListModel.invalidate();
+               }
+               this.changesListModel.setNewChangesExist( false );
+               this.updatingChangesList = true;
+               return this._fetchChangesList()
+                       .then(
+                               // Success
+                               function ( pieces ) {
+                                       var $changesListContent = pieces.changes,
+                                               $fieldset = pieces.fieldset;
+                                       this.changesListModel.update(
+                                               $changesListContent,
+                                               $fieldset,
+                                               pieces.noResultsDetails,
+                                               false,
+                                               // separator between old and new changes
+                                               updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
+                                       );
+                               }.bind( this )
+                               // Do nothing for failure
+                       )
+                       .always( function () {
+                               this.updatingChangesList = false;
+                       }.bind( this ) );
+       };
+
+       /**
+        * Get an object representing the default parameter state, whether
+        * it is from the model defaults or from the saved queries.
+        *
+        * @return {Object} Default parameters
+        */
+       Controller.prototype._getDefaultParams = function () {
+               if ( this.savedQueriesModel.getDefault() ) {
+                       return this.savedQueriesModel.getDefaultParams();
+               } else {
+                       return this.filtersModel.getDefaultParams();
+               }
+       };
+
+       /**
+        * Query the list of changes from the server for the current filters
+        *
+        * @param {string} counterId Id for this request. To allow concurrent requests
+        *  not to invalidate each other.
+        * @param {Object} [params={}] Parameters to add to the query
+        *
+        * @return {jQuery.Promise} Promise object resolved with { content, status }
+        */
+       Controller.prototype._queryChangesList = function ( counterId, params ) {
+               var uri = this.uriProcessor.getUpdatedUri(),
+                       stickyParams = this.filtersModel.getStickyParamsValues(),
+                       requestId,
+                       latestRequest;
+
+               params = params || {};
+               params.action = 'render'; // bypasses MW chrome
+
+               uri.extend( params );
+
+               this.requestCounter[ counterId ] = this.requestCounter[ counterId ] || 0;
+               requestId = ++this.requestCounter[ counterId ];
+               latestRequest = function () {
+                       return requestId === this.requestCounter[ counterId ];
+               }.bind( this );
+
+               // Sticky parameters override the URL params
+               // this is to make sure that whether we represent
+               // the sticky params in the URL or not (they may
+               // be normalized out) the sticky parameters are
+               // always being sent to the server with their
+               // current/default values
+               uri.extend( stickyParams );
+
+               return $.ajax( uri.toString(), { contentType: 'html' } )
+                       .then(
+                               function ( content, message, jqXHR ) {
+                                       if ( !latestRequest() ) {
+                                               return $.Deferred().reject();
+                                       }
+                                       return {
+                                               content: content,
+                                               status: jqXHR.status
+                                       };
+                               },
+                               // RC returns 404 when there is no results
+                               function ( jqXHR ) {
+                                       if ( latestRequest() ) {
+                                               return $.Deferred().resolve(
+                                                       {
+                                                               content: jqXHR.responseText,
+                                                               status: jqXHR.status
+                                                       }
+                                               ).promise();
+                                       }
+                               }
+                       );
+       };
+
+       /**
+        * Fetch the list of changes from the server for the current filters
+        *
+        * @return {jQuery.Promise} Promise object that will resolve with the changes list
+        *  and the fieldset.
+        */
+       Controller.prototype._fetchChangesList = function () {
+               return this._queryChangesList( 'updateChangesList' )
+                       .then(
+                               function ( data ) {
+                                       var $parsed;
+
+                                       // Status code 0 is not HTTP status code,
+                                       // but is valid value of XMLHttpRequest status.
+                                       // It is used for variety of network errors, for example
+                                       // when an AJAX call was cancelled before getting the response
+                                       if ( data && data.status === 0 ) {
+                                               return {
+                                                       changes: 'NO_RESULTS',
+                                                       // We need empty result set, to avoid exceptions because of undefined value
+                                                       fieldset: $( [] ),
+                                                       noResultsDetails: 'NO_RESULTS_NETWORK_ERROR'
+                                               };
+                                       }
+
+                                       $parsed = $( '<div>' ).append( $( $.parseHTML(
+                                               data ? data.content : ''
+                                       ) ) );
+
+                                       return this._extractChangesListInfo( $parsed, data.status );
+                               }.bind( this )
+                       );
+       };
+
+       /**
+        * Track usage of highlight feature
+        *
+        * @param {string} action
+        * @param {Array|Object|string} filters
+        */
+       Controller.prototype._trackHighlight = function ( action, filters ) {
+               filters = typeof filters === 'string' ? { name: filters } : filters;
+               filters = !Array.isArray( filters ) ? [ filters ] : filters;
+               mw.track(
+                       'event.ChangesListHighlights',
+                       {
+                               action: action,
+                               filters: filters,
+                               userId: mw.user.getId()
+                       }
+               );
+       };
+
+       /**
+        * Track filter grouping usage
+        *
+        * @param {string} action Action taken
+        */
+       Controller.prototype.trackFilterGroupings = function ( action ) {
+               var controller = this,
+                       rightNow = new Date().getTime(),
+                       randomIdentifier = String( mw.user.sessionId() ) + String( rightNow ) + String( Math.random() ),
+                       // Get all current filters
+                       filters = this.filtersModel.findSelectedItems().map( function ( item ) {
+                               return item.getName();
+                       } );
+
+               action = action || 'filtermenu';
+
+               // Check if these filters were the ones we just logged previously
+               // (Don't log the same grouping twice, in case the user opens/closes)
+               // the menu without action, or with the same result
+               if (
+                       // Only log if the two arrays are different in size
+                       filters.length !== this.prevLoggedItems.length ||
+                       // Or if any filters are not the same as the cached filters
+                       filters.some( function ( filterName ) {
+                               return controller.prevLoggedItems.indexOf( filterName ) === -1;
+                       } ) ||
+                       // Or if any cached filters are not the same as given filters
+                       this.prevLoggedItems.some( function ( filterName ) {
+                               return filters.indexOf( filterName ) === -1;
+                       } )
+               ) {
+                       filters.forEach( function ( filterName ) {
+                               mw.track(
+                                       'event.ChangesListFilterGrouping',
+                                       {
+                                               action: action,
+                                               groupIdentifier: randomIdentifier,
+                                               filter: filterName,
+                                               userId: mw.user.getId()
+                                       }
+                               );
+                       } );
+
+                       // Cache the filter names
+                       this.prevLoggedItems = filters;
+               }
+       };
+
+       /**
+        * Apply a change of parameters to the model state, and check whether
+        * the new state is different than the old state.
+        *
+        * @param  {Object} newParamState New parameter state to apply
+        * @return {boolean} New applied model state is different than the previous state
+        */
+       Controller.prototype.applyParamChange = function ( newParamState ) {
+               var after,
+                       before = this.filtersModel.getSelectedState();
+
+               this.filtersModel.updateStateFromParams( newParamState );
+
+               after = this.filtersModel.getSelectedState();
+
+               return !OO.compare( before, after );
+       };
+
+       /**
+        * Mark all changes as seen on Watchlist
+        */
+       Controller.prototype.markAllChangesAsSeen = function () {
+               var api = new mw.Api();
+               api.postWithToken( 'csrf', {
+                       formatversion: 2,
+                       action: 'setnotificationtimestamp',
+                       entirewatchlist: true
+               } ).then( function () {
+                       this.updateChangesList( null, 'markSeen' );
+               }.bind( this ) );
+       };
+
+       /**
+        * Set the current search for the system.
+        *
+        * @param {string} searchQuery Search query, including triggers
+        */
+       Controller.prototype.setSearch = function ( searchQuery ) {
+               this.filtersModel.setSearch( searchQuery );
+       };
+
+       /**
+        * Switch the view by changing the search query trigger
+        * without changing the search term
+        *
+        * @param  {string} view View to change to
+        */
+       Controller.prototype.switchView = function ( view ) {
+               this.setSearch(
+                       this.filtersModel.getViewTrigger( view ) +
+                       this.filtersModel.removeViewTriggers( this.filtersModel.getSearch() )
+               );
+       };
+
+       /**
+        * Reset the search for a specific view. This means we null the search query
+        * and replace it with the relevant trigger for the requested view
+        *
+        * @param  {string} [view='default'] View to change to
+        */
+       Controller.prototype.resetSearchForView = function ( view ) {
+               view = view || 'default';
+
+               this.setSearch(
+                       this.filtersModel.getViewTrigger( view )
+               );
+       };
+
+       module.exports = Controller;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/HighlightColors.js b/resources/src/mediawiki.rcfilters/HighlightColors.js
new file mode 100644 (file)
index 0000000..a4ef73b
--- /dev/null
@@ -0,0 +1,12 @@
+( function () {
+       /**
+        * Supported highlight colors.
+        * Warning: These are also hardcoded in "styles/mw.rcfilters.variables.less"
+        *
+        * @member mw.rcfilters
+        * @property {string[]}
+        */
+       var HighlightColors = [ 'c1', 'c2', 'c3', 'c4', 'c5' ];
+
+       module.exports = HighlightColors;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/UriProcessor.js b/resources/src/mediawiki.rcfilters/UriProcessor.js
new file mode 100644 (file)
index 0000000..37874d5
--- /dev/null
@@ -0,0 +1,296 @@
+( function () {
+       /* eslint no-underscore-dangle: "off" */
+       /**
+        * URI Processor for RCFilters
+        *
+        * @class mw.rcfilters.UriProcessor
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
+        * @param {Object} [config] Configuration object
+        * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
+        *  title normalization to separate title subpage/parts into the target= url
+        *  parameter
+        */
+       var UriProcessor = function MwRcfiltersController( filtersModel, config ) {
+               config = config || {};
+               this.filtersModel = filtersModel;
+
+               this.normalizeTarget = !!config.normalizeTarget;
+       };
+
+       /* Initialization */
+       OO.initClass( UriProcessor );
+
+       /* Static methods */
+
+       /**
+        * Replace the url history through replaceState
+        *
+        * @param {mw.Uri} newUri New URI to replace
+        */
+       UriProcessor.static.replaceState = function ( newUri ) {
+               window.history.replaceState(
+                       { tag: 'rcfilters' },
+                       document.title,
+                       newUri.toString()
+               );
+       };
+
+       /**
+        * Push the url to history through pushState
+        *
+        * @param {mw.Uri} newUri New URI to push
+        */
+       UriProcessor.static.pushState = function ( newUri ) {
+               window.history.pushState(
+                       { tag: 'rcfilters' },
+                       document.title,
+                       newUri.toString()
+               );
+       };
+
+       /* Methods */
+
+       /**
+        * Get the version that this URL query is tagged with.
+        *
+        * @param {Object} [uriQuery] URI query
+        * @return {number} URL version
+        */
+       UriProcessor.prototype.getVersion = function ( uriQuery ) {
+               uriQuery = uriQuery || new mw.Uri().query;
+
+               return Number( uriQuery.urlversion || 1 );
+       };
+
+       /**
+        * Get an updated mw.Uri object based on the model state
+        *
+        * @param {mw.Uri} [uri] An external URI to build the new uri
+        *  with. This is mainly for tests, to be able to supply external query
+        *  parameters and make sure they are retained.
+        * @return {mw.Uri} Updated Uri
+        */
+       UriProcessor.prototype.getUpdatedUri = function ( uri ) {
+               var normalizedUri = this._normalizeTargetInUri( uri || new mw.Uri() ),
+                       unrecognizedParams = this.getUnrecognizedParams( normalizedUri.query );
+
+               normalizedUri.query = this.filtersModel.getMinimizedParamRepresentation(
+                       $.extend(
+                               true,
+                               {},
+                               normalizedUri.query,
+                               // The representation must be expanded so it can
+                               // override the uri query params but we then output
+                               // a minimized version for the entire URI representation
+                               // for the method
+                               this.filtersModel.getExpandedParamRepresentation()
+                       )
+               );
+
+               // Reapply unrecognized params and url version
+               normalizedUri.query = $.extend(
+                       true,
+                       {},
+                       normalizedUri.query,
+                       unrecognizedParams,
+                       { urlversion: '2' }
+               );
+
+               return normalizedUri;
+       };
+
+       /**
+        * Move the subpage to the target parameter
+        *
+        * @param {mw.Uri} uri
+        * @return {mw.Uri}
+        * @private
+        */
+       UriProcessor.prototype._normalizeTargetInUri = function ( uri ) {
+               var parts,
+                       // matches [/wiki/]SpecialNS:RCL/[Namespace:]Title/Subpage/Subsubpage/etc
+                       re = /^((?:\/.+?\/)?.*?:.*?)\/(.*)$/;
+
+               if ( !this.normalizeTarget ) {
+                       return uri;
+               }
+
+               // target in title param
+               if ( uri.query.title ) {
+                       parts = uri.query.title.match( re );
+                       if ( parts ) {
+                               uri.query.title = parts[ 1 ];
+                               uri.query.target = parts[ 2 ];
+                       }
+               }
+
+               // target in path
+               parts = mw.Uri.decode( uri.path ).match( re );
+               if ( parts ) {
+                       uri.path = parts[ 1 ];
+                       uri.query.target = parts[ 2 ];
+               }
+
+               return uri;
+       };
+
+       /**
+        * Get an object representing given parameters that are unrecognized by the model
+        *
+        * @param  {Object} params Full params object
+        * @return {Object} Unrecognized params
+        */
+       UriProcessor.prototype.getUnrecognizedParams = function ( params ) {
+               // Start with full representation
+               var givenParamNames = Object.keys( params ),
+                       unrecognizedParams = $.extend( true, {}, params );
+
+               // Extract unrecognized parameters
+               Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) {
+                       // Remove recognized params
+                       if ( givenParamNames.indexOf( paramName ) > -1 ) {
+                               delete unrecognizedParams[ paramName ];
+                       }
+               } );
+
+               return unrecognizedParams;
+       };
+
+       /**
+        * Update the URL of the page to reflect current filters
+        *
+        * This should not be called directly from outside the controller.
+        * If an action requires changing the URL, it should either use the
+        * highlighting actions below, or call #updateChangesList which does
+        * the uri corrections already.
+        *
+        * @param {Object} [params] Extra parameters to add to the API call
+        */
+       UriProcessor.prototype.updateURL = function ( params ) {
+               var currentUri = new mw.Uri(),
+                       updatedUri = this.getUpdatedUri();
+
+               updatedUri.extend( params || {} );
+
+               if (
+                       this.getVersion( currentUri.query ) !== 2 ||
+                       this.isNewState( currentUri.query, updatedUri.query )
+               ) {
+                       this.constructor.static.replaceState( updatedUri );
+               }
+       };
+
+       /**
+        * Update the filters model based on the URI query
+        * This happens on initialization, and from this moment on,
+        * we consider the system synchronized, and the model serves
+        * as the source of truth for the URL.
+        *
+        * This methods should only be called once on initialization.
+        * After initialization, the model updates the URL, not the
+        * other way around.
+        *
+        * @param {Object} [uriQuery] URI query
+        */
+       UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
+               uriQuery = uriQuery || this._normalizeTargetInUri( new mw.Uri() ).query;
+               this.filtersModel.updateStateFromParams(
+                       this._getNormalizedQueryParams( uriQuery )
+               );
+       };
+
+       /**
+        * Compare two URI queries to decide whether they are different
+        * enough to represent a new state.
+        *
+        * @param {Object} currentUriQuery Current Uri query
+        * @param {Object} updatedUriQuery Updated Uri query
+        * @return {boolean} This is a new state
+        */
+       UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) {
+               var currentParamState, updatedParamState,
+                       notEquivalent = function ( obj1, obj2 ) {
+                               var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
+                               return keys.some( function ( key ) {
+                                       return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
+                               } );
+                       };
+
+               // Compare states instead of parameters
+               // This will allow us to always have a proper check of whether
+               // the requested new url is one to change or not, regardless of
+               // actual parameter visibility/representation in the URL
+               currentParamState = $.extend(
+                       true,
+                       {},
+                       this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ),
+                       this.getUnrecognizedParams( currentUriQuery )
+               );
+               updatedParamState = $.extend(
+                       true,
+                       {},
+                       this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ),
+                       this.getUnrecognizedParams( updatedUriQuery )
+               );
+
+               return notEquivalent( currentParamState, updatedParamState );
+       };
+
+       /**
+        * Check whether the given query has parameters that are
+        * recognized as parameters we should load the system with
+        *
+        * @param {mw.Uri} [uriQuery] Given URI query
+        * @return {boolean} Query contains valid recognized parameters
+        */
+       UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
+               var anyValidInUrl,
+                       validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() );
+
+               uriQuery = uriQuery || new mw.Uri().query;
+
+               anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) {
+                       return validParameterNames.indexOf( parameter ) > -1;
+               } );
+
+               // URL version 2 is allowed to be empty or within nonrecognized params
+               return anyValidInUrl || this.getVersion( uriQuery ) === 2;
+       };
+
+       /**
+        * Get the adjusted URI params based on the url version
+        * If the urlversion is not 2, the parameters are merged with
+        * the model's defaults.
+        * Always merge in the hidden parameter defaults.
+        *
+        * @private
+        * @param {Object} uriQuery Current URI query
+        * @return {Object} Normalized parameters
+        */
+       UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) {
+               // Check whether we are dealing with urlversion=2
+               // If we are, we do not merge the initial request with
+               // defaults. Not having urlversion=2 means we need to
+               // reproduce the server-side request and merge the
+               // requested parameters (or starting state) with the
+               // wiki default.
+               // Any subsequent change of the URL through the RCFilters
+               // system will receive 'urlversion=2'
+               var base = this.getVersion( uriQuery ) === 2 ?
+                       {} :
+                       this.filtersModel.getDefaultParams();
+
+               return $.extend(
+                       true,
+                       {},
+                       this.filtersModel.getMinimizedParamRepresentation(
+                               $.extend( true, {}, base, uriQuery )
+                       ),
+                       { urlversion: '2' }
+               );
+       };
+
+       module.exports = UriProcessor;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/ChangesListViewModel.js b/resources/src/mediawiki.rcfilters/dm/ChangesListViewModel.js
new file mode 100644 (file)
index 0000000..64d2e79
--- /dev/null
@@ -0,0 +1,169 @@
+( function () {
+       /**
+        * View model for the changes list
+        *
+        * @class mw.rcfilters.dm.ChangesListViewModel
+        * @mixins OO.EventEmitter
+        *
+        * @param {jQuery} $initialFieldset The initial server-generated legacy form content
+        * @constructor
+        */
+       var ChangesListViewModel = function MwRcfiltersDmChangesListViewModel( $initialFieldset ) {
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+
+               this.valid = true;
+               this.newChangesExist = false;
+               this.liveUpdate = false;
+               this.unseenWatchedChanges = false;
+
+               this.extractNextFrom( $initialFieldset );
+       };
+
+       /* Initialization */
+       OO.initClass( ChangesListViewModel );
+       OO.mixinClass( ChangesListViewModel, OO.EventEmitter );
+
+       /* Events */
+
+       /**
+        * @event invalidate
+        *
+        * The list of changes is now invalid (out of date)
+        */
+
+       /**
+        * @event update
+        * @param {jQuery|string} $changesListContent List of changes
+        * @param {jQuery} $fieldset Server-generated form
+        * @param {string} noResultsDetails Type of no result error
+        * @param {boolean} isInitialDOM Whether the previous dom variables are from the initial page load
+        * @param {boolean} fromLiveUpdate These are new changes fetched via Live Update
+        *
+        * The list of changes has been updated
+        */
+
+       /**
+        * @event newChangesExist
+        * @param {boolean} newChangesExist
+        *
+        * The existence of changes newer than those currently displayed has changed.
+        */
+
+       /**
+        * @event liveUpdateChange
+        * @param {boolean} enable
+        *
+        * The state of the 'live update' feature has changed.
+        */
+
+       /* Methods */
+
+       /**
+        * Invalidate the list of changes
+        *
+        * @fires invalidate
+        */
+       ChangesListViewModel.prototype.invalidate = function () {
+               if ( this.valid ) {
+                       this.valid = false;
+                       this.emit( 'invalidate' );
+               }
+       };
+
+       /**
+        * Update the model with an updated list of changes
+        *
+        * @param {jQuery|string} changesListContent
+        * @param {jQuery} $fieldset
+        * @param {string} noResultsDetails Type of no result error
+        * @param {boolean} [isInitialDOM] Using the initial (already attached) DOM elements
+        * @param {boolean} [separateOldAndNew] Whether a logical separation between old and new changes is needed
+        * @fires update
+        */
+       ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ) {
+               var from = this.nextFrom;
+               this.valid = true;
+               this.extractNextFrom( $fieldset );
+               this.checkForUnseenWatchedChanges( changesListContent );
+               this.emit( 'update', changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ? from : null );
+       };
+
+       /**
+        * Specify whether new changes exist
+        *
+        * @param {boolean} newChangesExist
+        * @fires newChangesExist
+        */
+       ChangesListViewModel.prototype.setNewChangesExist = function ( newChangesExist ) {
+               if ( newChangesExist !== this.newChangesExist ) {
+                       this.newChangesExist = newChangesExist;
+                       this.emit( 'newChangesExist', newChangesExist );
+               }
+       };
+
+       /**
+        * @return {boolean} Whether new changes exist
+        */
+       ChangesListViewModel.prototype.getNewChangesExist = function () {
+               return this.newChangesExist;
+       };
+
+       /**
+        * Extract the value of the 'from' parameter from a link in the field set
+        *
+        * @param {jQuery} $fieldset
+        */
+       ChangesListViewModel.prototype.extractNextFrom = function ( $fieldset ) {
+               var data = $fieldset.find( '.rclistfrom > a, .wlinfo' ).data( 'params' );
+               if ( data && data.from ) {
+                       this.nextFrom = data.from;
+               }
+       };
+
+       /**
+        * @return {string} The 'from' parameter that can be used to query new changes
+        */
+       ChangesListViewModel.prototype.getNextFrom = function () {
+               return this.nextFrom;
+       };
+
+       /**
+        * Toggle the 'live update' feature on/off
+        *
+        * @param {boolean} enable
+        */
+       ChangesListViewModel.prototype.toggleLiveUpdate = function ( enable ) {
+               enable = enable === undefined ? !this.liveUpdate : enable;
+               if ( enable !== this.liveUpdate ) {
+                       this.liveUpdate = enable;
+                       this.emit( 'liveUpdateChange', this.liveUpdate );
+               }
+       };
+
+       /**
+        * @return {boolean} The 'live update' feature is enabled
+        */
+       ChangesListViewModel.prototype.getLiveUpdate = function () {
+               return this.liveUpdate;
+       };
+
+       /**
+        * Check if some of the given changes watched and unseen
+        *
+        * @param {jQuery|string} changeslistContent
+        */
+       ChangesListViewModel.prototype.checkForUnseenWatchedChanges = function ( changeslistContent ) {
+               this.unseenWatchedChanges = changeslistContent !== 'NO_RESULTS' &&
+                       changeslistContent.find( '.mw-changeslist-line-watched' ).length > 0;
+       };
+
+       /**
+        * @return {boolean} Whether some of the current changes are watched and unseen
+        */
+       ChangesListViewModel.prototype.hasUnseenWatchedChanges = function () {
+               return this.unseenWatchedChanges;
+       };
+
+       module.exports = ChangesListViewModel;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/FilterGroup.js
new file mode 100644 (file)
index 0000000..831e6eb
--- /dev/null
@@ -0,0 +1,994 @@
+( function () {
+       var FilterItem = require( './FilterItem.js' ),
+               FilterGroup;
+
+       /**
+        * View model for a filter group
+        *
+        * @class mw.rcfilters.dm.FilterGroup
+        * @mixins OO.EventEmitter
+        * @mixins OO.EmitterList
+        *
+        * @constructor
+        * @param {string} name Group name
+        * @param {Object} [config] Configuration options
+        * @cfg {string} [type='send_unselected_if_any'] Group type
+        * @cfg {string} [view='default'] Name of the display group this group
+        *  is a part of.
+        * @cfg {boolean} [sticky] This group is 'sticky'. It is synchronized
+        *  with a preference, does not participate in Saved Queries, and is
+        *  not shown in the active filters area.
+        * @cfg {string} [title] Group title
+        * @cfg {boolean} [hidden] This group is hidden from the regular menu views
+        *  and the active filters area.
+        * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
+        *  group from the URL, even if it wasn't initially set up.
+        * @cfg {number} [range] An object defining minimum and maximum values for numeric
+        *  groups. { min: x, max: y }
+        * @cfg {number} [minValue] Minimum value for numeric groups
+        * @cfg {string} [separator='|'] Value separator for 'string_options' groups
+        * @cfg {boolean} [active] Group is active
+        * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
+        * @cfg {Object} [conflicts] Defines the conflicts for this filter group
+        * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
+        *  group. If the prefix has 'invert' state, the parameter is expected to be an object
+        *  with 'default' and 'inverted' as keys.
+        * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
+        * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
+        * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
+        * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
+        * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
+        * @cfg {boolean} [visible=true] The visibility of the group
+        */
+       FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+               OO.EmitterList.call( this );
+
+               this.name = name;
+               this.type = config.type || 'send_unselected_if_any';
+               this.view = config.view || 'default';
+               this.sticky = !!config.sticky;
+               this.title = config.title || name;
+               this.hidden = !!config.hidden;
+               this.allowArbitrary = !!config.allowArbitrary;
+               this.numericRange = config.range;
+               this.separator = config.separator || '|';
+               this.labelPrefixKey = config.labelPrefixKey;
+               this.visible = config.visible === undefined ? true : !!config.visible;
+
+               this.currSelected = null;
+               this.active = !!config.active;
+               this.fullCoverage = !!config.fullCoverage;
+
+               this.whatsThis = config.whatsThis || {};
+
+               this.conflicts = config.conflicts || {};
+               this.defaultParams = {};
+               this.defaultFilters = {};
+
+               this.aggregate( { update: 'filterItemUpdate' } );
+               this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
+       };
+
+       /* Initialization */
+       OO.initClass( FilterGroup );
+       OO.mixinClass( FilterGroup, OO.EventEmitter );
+       OO.mixinClass( FilterGroup, OO.EmitterList );
+
+       /* Events */
+
+       /**
+        * @event update
+        *
+        * Group state has been updated
+        */
+
+       /* Methods */
+
+       /**
+        * Initialize the group and create its filter items
+        *
+        * @param {Object} filterDefinition Filter definition for this group
+        * @param {string|Object} [groupDefault] Definition of the group default
+        */
+       FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
+               var defaultParam,
+                       supersetMap = {},
+                       model = this,
+                       items = [];
+
+               filterDefinition.forEach( function ( filter ) {
+                       // Instantiate an item
+                       var subsetNames = [],
+                               filterItem = new FilterItem( filter.name, model, {
+                                       group: model.getName(),
+                                       label: filter.label || filter.name,
+                                       description: filter.description || '',
+                                       labelPrefixKey: model.labelPrefixKey,
+                                       cssClass: filter.cssClass,
+                                       identifiers: filter.identifiers,
+                                       defaultHighlightColor: filter.defaultHighlightColor
+                               } );
+
+                       if ( filter.subset ) {
+                               filter.subset = filter.subset.map( function ( el ) {
+                                       return el.filter;
+                               } );
+
+                               subsetNames = [];
+
+                               filter.subset.forEach( function ( subsetFilterName ) {
+                                       // Subsets (unlike conflicts) are always inside the same group
+                                       // We can re-map the names of the filters we are getting from
+                                       // the subsets with the group prefix
+                                       var subsetName = model.getPrefixedName( subsetFilterName );
+                                       // For convenience, we should store each filter's "supersets" -- these are
+                                       // the filters that have that item in their subset list. This will just
+                                       // make it easier to go through whether the item has any other items
+                                       // that affect it (and are selected) at any given time
+                                       supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
+                                       mw.rcfilters.utils.addArrayElementsUnique(
+                                               supersetMap[ subsetName ],
+                                               filterItem.getName()
+                                       );
+
+                                       // Translate subset param name to add the group name, so we
+                                       // get consistent naming. We know that subsets are only within
+                                       // the same group
+                                       subsetNames.push( subsetName );
+                               } );
+
+                               // Set translated subset
+                               filterItem.setSubset( subsetNames );
+                       }
+
+                       items.push( filterItem );
+
+                       // Store default parameter state; in this case, default is defined per filter
+                       if (
+                               model.getType() === 'send_unselected_if_any' ||
+                               model.getType() === 'boolean'
+                       ) {
+                               // Store the default parameter state
+                               // For this group type, parameter values are direct
+                               // We need to convert from a boolean to a string ('1' and '0')
+                               model.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
+                       } else if ( model.getType() === 'any_value' ) {
+                               model.defaultParams[ filter.name ] = filter.default;
+                       }
+               } );
+
+               // Add items
+               this.addItems( items );
+
+               // Now that we have all items, we can apply the superset map
+               this.getItems().forEach( function ( filterItem ) {
+                       filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
+               } );
+
+               // Store default parameter state; in this case, default is defined per the
+               // entire group, given by groupDefault method parameter
+               if ( this.getType() === 'string_options' ) {
+                       // Store the default parameter group state
+                       // For this group, the parameter is group name and value is the names
+                       // of selected items
+                       this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
+                               // Current values
+                               groupDefault ?
+                                       groupDefault.split( this.getSeparator() ) :
+                                       [],
+                               // Legal values
+                               this.getItems().map( function ( item ) {
+                                       return item.getParamName();
+                               } )
+                       ).join( this.getSeparator() );
+               } else if ( this.getType() === 'single_option' ) {
+                       defaultParam = groupDefault !== undefined ?
+                               groupDefault : this.getItems()[ 0 ].getParamName();
+
+                       // For this group, the parameter is the group name,
+                       // and a single item can be selected: default or first item
+                       this.defaultParams[ this.getName() ] = defaultParam;
+               }
+
+               // add highlights to defaultParams
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( filterItem.isHighlighted() ) {
+                               this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
+                       }
+               }.bind( this ) );
+
+               // Store default filter state based on default params
+               this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
+
+               // Check for filters that should be initially selected by their default value
+               if ( this.isSticky() ) {
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( this.defaultFilters, function ( filterName, filterValue ) {
+                               model.getItemByName( filterName ).toggleSelected( filterValue );
+                       } );
+               }
+
+               // Verify that single_option group has at least one item selected
+               if (
+                       this.getType() === 'single_option' &&
+                       this.findSelectedItems().length === 0
+               ) {
+                       defaultParam = groupDefault !== undefined ?
+                               groupDefault : this.getItems()[ 0 ].getParamName();
+
+                       // Single option means there must be a single option
+                       // selected, so we have to either select the default
+                       // or select the first option
+                       this.selectItemByParamName( defaultParam );
+               }
+       };
+
+       /**
+        * Respond to filterItem update event
+        *
+        * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
+        * @fires update
+        */
+       FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
+               // Update state
+               var changed = false,
+                       active = this.areAnySelected(),
+                       model = this;
+
+               if ( this.getType() === 'single_option' ) {
+                       // This group must have one item selected always
+                       // and must never have more than one item selected at a time
+                       if ( this.findSelectedItems().length === 0 ) {
+                               // Nothing is selected anymore
+                               // Select the default or the first item
+                               this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+                                       this.getItems()[ 0 ];
+                               this.currSelected.toggleSelected( true );
+                               changed = true;
+                       } else if ( this.findSelectedItems().length > 1 ) {
+                               // There is more than one item selected
+                               // This should only happen if the item given
+                               // is the one that is selected, so unselect
+                               // all items that is not it
+                               this.findSelectedItems().forEach( function ( itemModel ) {
+                                       // Note that in case the given item is actually
+                                       // not selected, this loop will end up unselecting
+                                       // all items, which would trigger the case above
+                                       // when the last item is unselected anyways
+                                       var selected = itemModel.getName() === item.getName() &&
+                                               item.isSelected();
+
+                                       itemModel.toggleSelected( selected );
+                                       if ( selected ) {
+                                               model.currSelected = itemModel;
+                                       }
+                               } );
+                               changed = true;
+                       }
+               }
+
+               if ( this.isSticky() ) {
+                       // If this group is sticky, then change the default according to the
+                       // current selection.
+                       this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
+               }
+
+               if (
+                       changed ||
+                       this.active !== active ||
+                       this.currSelected !== item
+               ) {
+                       this.active = active;
+                       this.currSelected = item;
+
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Get group active state
+        *
+        * @return {boolean} Active state
+        */
+       FilterGroup.prototype.isActive = function () {
+               return this.active;
+       };
+
+       /**
+        * Get group hidden state
+        *
+        * @return {boolean} Hidden state
+        */
+       FilterGroup.prototype.isHidden = function () {
+               return this.hidden;
+       };
+
+       /**
+        * Get group allow arbitrary state
+        *
+        * @return {boolean} Group allows an arbitrary value from the URL
+        */
+       FilterGroup.prototype.isAllowArbitrary = function () {
+               return this.allowArbitrary;
+       };
+
+       /**
+        * Get group maximum value for numeric groups
+        *
+        * @return {number|null} Group max value
+        */
+       FilterGroup.prototype.getMaxValue = function () {
+               return this.numericRange && this.numericRange.max !== undefined ?
+                       this.numericRange.max : null;
+       };
+
+       /**
+        * Get group minimum value for numeric groups
+        *
+        * @return {number|null} Group max value
+        */
+       FilterGroup.prototype.getMinValue = function () {
+               return this.numericRange && this.numericRange.min !== undefined ?
+                       this.numericRange.min : null;
+       };
+
+       /**
+        * Get group name
+        *
+        * @return {string} Group name
+        */
+       FilterGroup.prototype.getName = function () {
+               return this.name;
+       };
+
+       /**
+        * Get the default param state of this group
+        *
+        * @return {Object} Default param state
+        */
+       FilterGroup.prototype.getDefaultParams = function () {
+               return this.defaultParams;
+       };
+
+       /**
+        * Get the default filter state of this group
+        *
+        * @return {Object} Default filter state
+        */
+       FilterGroup.prototype.getDefaultFilters = function () {
+               return this.defaultFilters;
+       };
+
+       /**
+        * This is for a single_option and string_options group types
+        * it returns the value of the default
+        *
+        * @return {string} Value of the default
+        */
+       FilterGroup.prototype.getDefaulParamValue = function () {
+               return this.defaultParams[ this.getName() ];
+       };
+       /**
+        * Get the messags defining the 'whats this' popup for this group
+        *
+        * @return {Object} What's this messages
+        */
+       FilterGroup.prototype.getWhatsThis = function () {
+               return this.whatsThis;
+       };
+
+       /**
+        * Check whether this group has a 'what's this' message
+        *
+        * @return {boolean} This group has a what's this message
+        */
+       FilterGroup.prototype.hasWhatsThis = function () {
+               return !!this.whatsThis.body;
+       };
+
+       /**
+        * Get the conflicts associated with the entire group.
+        * Conflict object is set up by filter name keys and conflict
+        * definition. For example:
+        * [
+        *     {
+        *         filterName: {
+        *             filter: filterName,
+        *             group: group1
+        *         }
+        *     },
+        *     {
+        *         filterName2: {
+        *             filter: filterName2,
+        *             group: group2
+        *         }
+        *     }
+        * ]
+        * @return {Object} Conflict definition
+        */
+       FilterGroup.prototype.getConflicts = function () {
+               return this.conflicts;
+       };
+
+       /**
+        * Set conflicts for this group. See #getConflicts for the expected
+        * structure of the definition.
+        *
+        * @param {Object} conflicts Conflicts for this group
+        */
+       FilterGroup.prototype.setConflicts = function ( conflicts ) {
+               this.conflicts = conflicts;
+       };
+
+       /**
+        * Set conflicts for each filter item in the group based on the
+        * given conflict map
+        *
+        * @param {Object} conflicts Object representing the conflict map,
+        *  keyed by the item name, where its value is an object for all its conflicts
+        */
+       FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( conflicts[ filterItem.getName() ] ) {
+                               filterItem.setConflicts( conflicts[ filterItem.getName() ] );
+                       }
+               } );
+       };
+
+       /**
+        * Check whether this item has a potential conflict with the given item
+        *
+        * This checks whether the given item is in the list of conflicts of
+        * the current item, but makes no judgment about whether the conflict
+        * is currently at play (either one of the items may not be selected)
+        *
+        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
+        * @return {boolean} This item has a conflict with the given item
+        */
+       FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
+               return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
+       };
+
+       /**
+        * Check whether there are any items selected
+        *
+        * @return {boolean} Any items in the group are selected
+        */
+       FilterGroup.prototype.areAnySelected = function () {
+               return this.getItems().some( function ( filterItem ) {
+                       return filterItem.isSelected();
+               } );
+       };
+
+       /**
+        * Check whether all items selected
+        *
+        * @return {boolean} All items are selected
+        */
+       FilterGroup.prototype.areAllSelected = function () {
+               var selected = [],
+                       unselected = [];
+
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( filterItem.isSelected() ) {
+                               selected.push( filterItem );
+                       } else {
+                               unselected.push( filterItem );
+                       }
+               } );
+
+               if ( unselected.length === 0 ) {
+                       return true;
+               }
+
+               // check if every unselected is a subset of a selected
+               return unselected.every( function ( unselectedFilterItem ) {
+                       return selected.some( function ( selectedFilterItem ) {
+                               return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
+                       } );
+               } );
+       };
+
+       /**
+        * Get all selected items in this group
+        *
+        * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
+        * @return {mw.rcfilters.dm.FilterItem[]} Selected items
+        */
+       FilterGroup.prototype.findSelectedItems = function ( excludeItem ) {
+               var excludeName = ( excludeItem && excludeItem.getName() ) || '';
+
+               return this.getItems().filter( function ( item ) {
+                       return item.getName() !== excludeName && item.isSelected();
+               } );
+       };
+
+       /**
+        * Check whether all selected items are in conflict with the given item
+        *
+        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
+        * @return {boolean} All selected items are in conflict with this item
+        */
+       FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
+               var selectedItems = this.findSelectedItems( filterItem );
+
+               return selectedItems.length > 0 &&
+                       (
+                               // The group as a whole is in conflict with this item
+                               this.existsInConflicts( filterItem ) ||
+                               // All selected items are in conflict individually
+                               selectedItems.every( function ( selectedFilter ) {
+                                       return selectedFilter.existsInConflicts( filterItem );
+                               } )
+                       );
+       };
+
+       /**
+        * Check whether any of the selected items are in conflict with the given item
+        *
+        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
+        * @return {boolean} Any of the selected items are in conflict with this item
+        */
+       FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
+               var selectedItems = this.findSelectedItems( filterItem );
+
+               return selectedItems.length > 0 && (
+                       // The group as a whole is in conflict with this item
+                       this.existsInConflicts( filterItem ) ||
+                       // Any selected items are in conflict individually
+                       selectedItems.some( function ( selectedFilter ) {
+                               return selectedFilter.existsInConflicts( filterItem );
+                       } )
+               );
+       };
+
+       /**
+        * Get the parameter representation from this group
+        *
+        * @param {Object} [filterRepresentation] An object defining the state
+        *  of the filters in this group, keyed by their name and current selected
+        *  state value.
+        * @return {Object} Parameter representation
+        */
+       FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
+               var values,
+                       areAnySelected = false,
+                       buildFromCurrentState = !filterRepresentation,
+                       defaultFilters = this.getDefaultFilters(),
+                       result = {},
+                       model = this,
+                       filterParamNames = {},
+                       getSelectedParameter = function ( filters ) {
+                               var item,
+                                       selected = [];
+
+                               // Find if any are selected
+                               // eslint-disable-next-line jquery/no-each-util
+                               $.each( filters, function ( name, value ) {
+                                       if ( value ) {
+                                               selected.push( name );
+                                       }
+                               } );
+
+                               item = model.getItemByName( selected[ 0 ] );
+                               return ( item && item.getParamName() ) || '';
+                       };
+
+               filterRepresentation = filterRepresentation || {};
+
+               // Create or complete the filterRepresentation definition
+               this.getItems().forEach( function ( item ) {
+                       // Map filter names to their parameter names
+                       filterParamNames[ item.getName() ] = item.getParamName();
+
+                       if ( buildFromCurrentState ) {
+                               // This means we have not been given a filter representation
+                               // so we are building one based on current state
+                               filterRepresentation[ item.getName() ] = item.getValue();
+                       } else if ( filterRepresentation[ item.getName() ] === undefined ) {
+                               // We are given a filter representation, but we have to make
+                               // sure that we fill in the missing filters if there are any
+                               // we will assume they are all falsey
+                               if ( model.isSticky() ) {
+                                       filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
+                               } else {
+                                       filterRepresentation[ item.getName() ] = false;
+                               }
+                       }
+
+                       if ( filterRepresentation[ item.getName() ] ) {
+                               areAnySelected = true;
+                       }
+               } );
+
+               // Build result
+               if (
+                       this.getType() === 'send_unselected_if_any' ||
+                       this.getType() === 'boolean' ||
+                       this.getType() === 'any_value'
+               ) {
+                       // First, check if any of the items are selected at all.
+                       // If none is selected, we're treating it as if they are
+                       // all false
+
+                       // Go over the items and define the correct values
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( filterRepresentation, function ( name, value ) {
+                               // We must store all parameter values as strings '0' or '1'
+                               if ( model.getType() === 'send_unselected_if_any' ) {
+                                       result[ filterParamNames[ name ] ] = areAnySelected ?
+                                               String( Number( !value ) ) :
+                                               '0';
+                               } else if ( model.getType() === 'boolean' ) {
+                                       // Representation is straight-forward and direct from
+                                       // the parameter value to the filter state
+                                       result[ filterParamNames[ name ] ] = String( Number( !!value ) );
+                               } else if ( model.getType() === 'any_value' ) {
+                                       result[ filterParamNames[ name ] ] = value;
+                               }
+                       } );
+               } else if ( this.getType() === 'string_options' ) {
+                       values = [];
+
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( filterRepresentation, function ( name, value ) {
+                               // Collect values
+                               if ( value ) {
+                                       values.push( filterParamNames[ name ] );
+                               }
+                       } );
+
+                       result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
+                               'all' : values.join( this.getSeparator() );
+               } else if ( this.getType() === 'single_option' ) {
+                       result[ this.getName() ] = getSelectedParameter( filterRepresentation );
+               }
+
+               return result;
+       };
+
+       /**
+        * Get the filter representation this group would provide
+        * based on given parameter states.
+        *
+        * @param {Object} [paramRepresentation] An object defining a parameter
+        *  state to translate the filter state from. If not given, an object
+        *  representing all filters as falsey is returned; same as if the parameter
+        *  given were an empty object, or had some of the filters missing.
+        * @return {Object} Filter representation
+        */
+       FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
+               var areAnySelected, paramValues, item, currentValue,
+                       oneWasSelected = false,
+                       defaultParams = this.getDefaultParams(),
+                       expandedParams = $.extend( true, {}, paramRepresentation ),
+                       model = this,
+                       paramToFilterMap = {},
+                       result = {};
+
+               if ( this.isSticky() ) {
+                       // If the group is sticky, check if all parameters are represented
+                       // and for those that aren't represented, add them with their default
+                       // values
+                       paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+               }
+
+               paramRepresentation = paramRepresentation || {};
+               if (
+                       this.getType() === 'send_unselected_if_any' ||
+                       this.getType() === 'boolean' ||
+                       this.getType() === 'any_value'
+               ) {
+                       // Go over param representation; map and check for selections
+                       this.getItems().forEach( function ( filterItem ) {
+                               var paramName = filterItem.getParamName();
+
+                               expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
+                               paramToFilterMap[ paramName ] = filterItem;
+
+                               if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
+                                       areAnySelected = true;
+                               }
+                       } );
+
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( expandedParams, function ( paramName, paramValue ) {
+                               var filterItem = paramToFilterMap[ paramName ];
+
+                               if ( model.getType() === 'send_unselected_if_any' ) {
+                                       // Flip the definition between the parameter
+                                       // state and the filter state
+                                       // This is what the 'toggleSelected' value of the filter is
+                                       result[ filterItem.getName() ] = areAnySelected ?
+                                               !Number( paramValue ) :
+                                               // Otherwise, there are no selected items in the
+                                               // group, which means the state is false
+                                               false;
+                               } else if ( model.getType() === 'boolean' ) {
+                                       // Straight-forward definition of state
+                                       result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
+                               } else if ( model.getType() === 'any_value' ) {
+                                       result[ filterItem.getName() ] = paramRepresentation[ filterItem.getParamName() ];
+                               }
+                       } );
+               } else if ( this.getType() === 'string_options' ) {
+                       currentValue = paramRepresentation[ this.getName() ] || '';
+
+                       // Normalize the given parameter values
+                       paramValues = mw.rcfilters.utils.normalizeParamOptions(
+                               // Given
+                               currentValue.split(
+                                       this.getSeparator()
+                               ),
+                               // Allowed values
+                               this.getItems().map( function ( filterItem ) {
+                                       return filterItem.getParamName();
+                               } )
+                       );
+                       // Translate the parameter values into a filter selection state
+                       this.getItems().forEach( function ( filterItem ) {
+                               // All true (either because all values are written or the term 'all' is written)
+                               // is the same as all filters set to true
+                               result[ filterItem.getName() ] = (
+                                       // If it is the word 'all'
+                                       paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
+                                       // All values are written
+                                       paramValues.length === model.getItemCount()
+                               ) ?
+                                       true :
+                                       // Otherwise, the filter is selected only if it appears in the parameter values
+                                       paramValues.indexOf( filterItem.getParamName() ) > -1;
+                       } );
+               } else if ( this.getType() === 'single_option' ) {
+                       // There is parameter that fits a single filter and if not, get the default
+                       this.getItems().forEach( function ( filterItem ) {
+                               var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
+
+                               result[ filterItem.getName() ] = selected;
+                               oneWasSelected = oneWasSelected || selected;
+                       } );
+               }
+
+               // Go over result and make sure all filters are represented.
+               // If any filters are missing, they will get a falsey value
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( result[ filterItem.getName() ] === undefined ) {
+                               result[ filterItem.getName() ] = this.getFalsyValue();
+                       }
+               }.bind( this ) );
+
+               // Make sure that at least one option is selected in
+               // single_option groups, no matter what path was taken
+               // If none was selected by the given definition, then
+               // we need to select the one in the base state -- either
+               // the default given, or the first item
+               if (
+                       this.getType() === 'single_option' &&
+                       !oneWasSelected
+               ) {
+                       item = this.getItems()[ 0 ];
+                       if ( defaultParams[ this.getName() ] ) {
+                               item = this.getItemByParamName( defaultParams[ this.getName() ] );
+                       }
+
+                       result[ item.getName() ] = true;
+               }
+
+               return result;
+       };
+
+       /**
+        * @return {*} The appropriate falsy value for this group type
+        */
+       FilterGroup.prototype.getFalsyValue = function () {
+               return this.getType() === 'any_value' ? '' : false;
+       };
+
+       /**
+        * Get current selected state of all filter items in this group
+        *
+        * @return {Object} Selected state
+        */
+       FilterGroup.prototype.getSelectedState = function () {
+               var state = {};
+
+               this.getItems().forEach( function ( filterItem ) {
+                       state[ filterItem.getName() ] = filterItem.getValue();
+               } );
+
+               return state;
+       };
+
+       /**
+        * Get item by its filter name
+        *
+        * @param {string} filterName Filter name
+        * @return {mw.rcfilters.dm.FilterItem} Filter item
+        */
+       FilterGroup.prototype.getItemByName = function ( filterName ) {
+               return this.getItems().filter( function ( item ) {
+                       return item.getName() === filterName;
+               } )[ 0 ];
+       };
+
+       /**
+        * Select an item by its parameter name
+        *
+        * @param {string} paramName Filter parameter name
+        */
+       FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
+               this.getItems().forEach( function ( item ) {
+                       item.toggleSelected( item.getParamName() === String( paramName ) );
+               } );
+       };
+
+       /**
+        * Get item by its parameter name
+        *
+        * @param {string} paramName Parameter name
+        * @return {mw.rcfilters.dm.FilterItem} Filter item
+        */
+       FilterGroup.prototype.getItemByParamName = function ( paramName ) {
+               return this.getItems().filter( function ( item ) {
+                       return item.getParamName() === String( paramName );
+               } )[ 0 ];
+       };
+
+       /**
+        * Get group type
+        *
+        * @return {string} Group type
+        */
+       FilterGroup.prototype.getType = function () {
+               return this.type;
+       };
+
+       /**
+        * Check whether this group is represented by a single parameter
+        * or whether each item is its own parameter
+        *
+        * @return {boolean} This group is a single parameter
+        */
+       FilterGroup.prototype.isPerGroupRequestParameter = function () {
+               return (
+                       this.getType() === 'string_options' ||
+                       this.getType() === 'single_option'
+               );
+       };
+
+       /**
+        * Get display group
+        *
+        * @return {string} Display group
+        */
+       FilterGroup.prototype.getView = function () {
+               return this.view;
+       };
+
+       /**
+        * Get the prefix used for the filter names inside this group.
+        *
+        * @param {string} [name] Filter name to prefix
+        * @return {string} Group prefix
+        */
+       FilterGroup.prototype.getNamePrefix = function () {
+               return this.getName() + '__';
+       };
+
+       /**
+        * Get a filter name with the prefix used for the filter names inside this group.
+        *
+        * @param {string} name Filter name to prefix
+        * @return {string} Group prefix
+        */
+       FilterGroup.prototype.getPrefixedName = function ( name ) {
+               return this.getNamePrefix() + name;
+       };
+
+       /**
+        * Get group's title
+        *
+        * @return {string} Title
+        */
+       FilterGroup.prototype.getTitle = function () {
+               return this.title;
+       };
+
+       /**
+        * Get group's values separator
+        *
+        * @return {string} Values separator
+        */
+       FilterGroup.prototype.getSeparator = function () {
+               return this.separator;
+       };
+
+       /**
+        * Check whether the group is defined as full coverage
+        *
+        * @return {boolean} Group is full coverage
+        */
+       FilterGroup.prototype.isFullCoverage = function () {
+               return this.fullCoverage;
+       };
+
+       /**
+        * Check whether the group is defined as sticky default
+        *
+        * @return {boolean} Group is sticky default
+        */
+       FilterGroup.prototype.isSticky = function () {
+               return this.sticky;
+       };
+
+       /**
+        * Normalize a value given to this group. This is mostly for correcting
+        * arbitrary values for 'single option' groups, given by the user settings
+        * or the URL that can go outside the limits that are allowed.
+        *
+        * @param  {string} value Given value
+        * @return {string} Corrected value
+        */
+       FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
+               if (
+                       this.getType() === 'single_option' &&
+                       this.isAllowArbitrary()
+               ) {
+                       if (
+                               this.getMaxValue() !== null &&
+                               value > this.getMaxValue()
+                       ) {
+                               // Change the value to the actual max value
+                               return String( this.getMaxValue() );
+                       } else if (
+                               this.getMinValue() !== null &&
+                               value < this.getMinValue()
+                       ) {
+                               // Change the value to the actual min value
+                               return String( this.getMinValue() );
+                       }
+               }
+
+               return value;
+       };
+
+       /**
+        * Toggle the visibility of this group
+        *
+        * @param {boolean} [isVisible] Item is visible
+        */
+       FilterGroup.prototype.toggleVisible = function ( isVisible ) {
+               isVisible = isVisible === undefined ? !this.visible : isVisible;
+
+               if ( this.visible !== isVisible ) {
+                       this.visible = isVisible;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Check whether the group is visible
+        *
+        * @return {boolean} Group is visible
+        */
+       FilterGroup.prototype.isVisible = function () {
+               return this.visible;
+       };
+
+       /**
+        * Set the visibility of the items under this group by the given items array
+        *
+        * @param {mw.rcfilters.dm.ItemModel[]} visibleItems An array of visible items
+        */
+       FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
+               this.getItems().forEach( function ( itemModel ) {
+                       itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
+               } );
+       };
+
+       module.exports = FilterGroup;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/FilterItem.js b/resources/src/mediawiki.rcfilters/dm/FilterItem.js
new file mode 100644 (file)
index 0000000..3e11d1e
--- /dev/null
@@ -0,0 +1,406 @@
+( function () {
+       var ItemModel = require( './ItemModel.js' ),
+               FilterItem;
+
+       /**
+        * Filter item model
+        *
+        * @class mw.rcfilters.dm.FilterItem
+        * @extends mw.rcfilters.dm.ItemModel
+        *
+        * @constructor
+        * @param {string} param Filter param name
+        * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
+        * @param {Object} config Configuration object
+        * @cfg {string[]} [excludes=[]] A list of filter names this filter, if
+        *  selected, makes inactive.
+        * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
+        * @cfg {Object} [conflicts] Defines the conflicts for this filter
+        * @cfg {boolean} [visible=true] The visibility of the group
+        */
+       FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
+               config = config || {};
+
+               this.groupModel = groupModel;
+
+               // Parent
+               FilterItem.parent.call( this, param, $.extend( {
+                       namePrefix: this.groupModel.getNamePrefix()
+               }, config ) );
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+
+               // Interaction definitions
+               this.subset = config.subset || [];
+               this.conflicts = config.conflicts || {};
+               this.superset = [];
+               this.visible = config.visible === undefined ? true : !!config.visible;
+
+               // Interaction states
+               this.included = false;
+               this.conflicted = false;
+               this.fullyCovered = false;
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FilterItem, ItemModel );
+
+       /* Methods */
+
+       /**
+        * Return the representation of the state of this item.
+        *
+        * @return {Object} State of the object
+        */
+       FilterItem.prototype.getState = function () {
+               return {
+                       selected: this.isSelected(),
+                       included: this.isIncluded(),
+                       conflicted: this.isConflicted(),
+                       fullyCovered: this.isFullyCovered()
+               };
+       };
+
+       /**
+        * Get the message for the display area for the currently active conflict
+        *
+        * @private
+        * @return {string} Conflict result message key
+        */
+       FilterItem.prototype.getCurrentConflictResultMessage = function () {
+               var details = {};
+
+               // First look in filter's own conflicts
+               details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
+               if ( !details.message ) {
+                       // Fall back onto conflicts in the group
+                       details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
+               }
+
+               return details.message;
+       };
+
+       /**
+        * Get the details of the active conflict on this filter
+        *
+        * @private
+        * @param {Object} conflicts Conflicts to examine
+        * @param {string} [key='contextDescription'] Message key
+        * @return {Object} Object with conflict message and conflict items
+        * @return {string} return.message Conflict message
+        * @return {string[]} return.names Conflicting item labels
+        */
+       FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
+               var group,
+                       conflictMessage = '',
+                       itemLabels = [];
+
+               key = key || 'contextDescription';
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( conflicts, function ( filterName, conflict ) {
+                       if ( !conflict.item.isSelected() ) {
+                               return;
+                       }
+
+                       if ( !conflictMessage ) {
+                               conflictMessage = conflict[ key ];
+                               group = conflict.group;
+                       }
+
+                       if ( group === conflict.group ) {
+                               itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
+                       }
+               } );
+
+               return {
+                       message: conflictMessage,
+                       names: itemLabels
+               };
+
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterItem.prototype.getStateMessage = function () {
+               var messageKey, details, superset,
+                       affectingItems = [];
+
+               if ( this.isSelected() ) {
+                       if ( this.isConflicted() ) {
+                               // First look in filter's own conflicts
+                               details = this.getConflictDetails( this.getOwnConflicts() );
+                               if ( !details.message ) {
+                                       // Fall back onto conflicts in the group
+                                       details = this.getConflictDetails( this.getGroupModel().getConflicts() );
+                               }
+
+                               messageKey = details.message;
+                               affectingItems = details.names;
+                       } else if ( this.isIncluded() && !this.isHighlighted() ) {
+                               // We only show the 'no effect' full-coverage message
+                               // if the item is also not highlighted. See T161273
+                               superset = this.getSuperset();
+                               // For this message we need to collect the affecting superset
+                               affectingItems = this.getGroupModel().findSelectedItems( this )
+                                       .filter( function ( item ) {
+                                               return superset.indexOf( item.getName() ) !== -1;
+                                       } )
+                                       .map( function ( item ) {
+                                               return mw.msg( 'quotation-marks', item.getLabel() );
+                                       } );
+
+                               messageKey = 'rcfilters-state-message-subset';
+                       } else if ( this.isFullyCovered() && !this.isHighlighted() ) {
+                               affectingItems = this.getGroupModel().findSelectedItems( this )
+                                       .map( function ( item ) {
+                                               return mw.msg( 'quotation-marks', item.getLabel() );
+                                       } );
+
+                               messageKey = 'rcfilters-state-message-fullcoverage';
+                       }
+               }
+
+               if ( messageKey ) {
+                       // Build message
+                       return mw.msg(
+                               messageKey,
+                               mw.language.listToText( affectingItems ),
+                               affectingItems.length
+                       );
+               }
+
+               // Display description
+               return this.getDescription();
+       };
+
+       /**
+        * Get the model of the group this filter belongs to
+        *
+        * @return {mw.rcfilters.dm.FilterGroup} Filter group model
+        */
+       FilterItem.prototype.getGroupModel = function () {
+               return this.groupModel;
+       };
+
+       /**
+        * Get the group name this filter belongs to
+        *
+        * @return {string} Filter group name
+        */
+       FilterItem.prototype.getGroupName = function () {
+               return this.groupModel.getName();
+       };
+
+       /**
+        * Get filter subset
+        * This is a list of filter names that are defined to be included
+        * when this filter is selected.
+        *
+        * @return {string[]} Filter subset
+        */
+       FilterItem.prototype.getSubset = function () {
+               return this.subset;
+       };
+
+       /**
+        * Get filter superset
+        * This is a generated list of filters that define this filter
+        * to be included when either of them is selected.
+        *
+        * @return {string[]} Filter superset
+        */
+       FilterItem.prototype.getSuperset = function () {
+               return this.superset;
+       };
+
+       /**
+        * Check whether the filter is currently in a conflict state
+        *
+        * @return {boolean} Filter is in conflict state
+        */
+       FilterItem.prototype.isConflicted = function () {
+               return this.conflicted;
+       };
+
+       /**
+        * Check whether the filter is currently in an already included subset
+        *
+        * @return {boolean} Filter is in an already-included subset
+        */
+       FilterItem.prototype.isIncluded = function () {
+               return this.included;
+       };
+
+       /**
+        * Check whether the filter is currently fully covered
+        *
+        * @return {boolean} Filter is in fully-covered state
+        */
+       FilterItem.prototype.isFullyCovered = function () {
+               return this.fullyCovered;
+       };
+
+       /**
+        * Get all conflicts associated with this filter or its group
+        *
+        * Conflict object is set up by filter name keys and conflict
+        * definition. For example:
+        *
+        *  {
+        *      filterName: {
+        *          filter: filterName,
+        *          group: group1,
+        *          label: itemLabel,
+        *          item: itemModel
+        *      }
+        *      filterName2: {
+        *          filter: filterName2,
+        *          group: group2
+        *          label: itemLabel2,
+        *          item: itemModel2
+        *      }
+        *  }
+        *
+        * @return {Object} Filter conflicts
+        */
+       FilterItem.prototype.getConflicts = function () {
+               return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
+       };
+
+       /**
+        * Get the conflicts associated with this filter
+        *
+        * @return {Object} Filter conflicts
+        */
+       FilterItem.prototype.getOwnConflicts = function () {
+               return this.conflicts;
+       };
+
+       /**
+        * Set conflicts for this filter. See #getConflicts for the expected
+        * structure of the definition.
+        *
+        * @param {Object} conflicts Conflicts for this filter
+        */
+       FilterItem.prototype.setConflicts = function ( conflicts ) {
+               this.conflicts = conflicts || {};
+       };
+
+       /**
+        * Set filter superset
+        *
+        * @param {string[]} superset Filter superset
+        */
+       FilterItem.prototype.setSuperset = function ( superset ) {
+               this.superset = superset || [];
+       };
+
+       /**
+        * Set filter subset
+        *
+        * @param {string[]} subset Filter subset
+        */
+       FilterItem.prototype.setSubset = function ( subset ) {
+               this.subset = subset || [];
+       };
+
+       /**
+        * Check whether a filter exists in the subset list for this filter
+        *
+        * @param {string} filterName Filter name
+        * @return {boolean} Filter name is in the subset list
+        */
+       FilterItem.prototype.existsInSubset = function ( filterName ) {
+               return this.subset.indexOf( filterName ) > -1;
+       };
+
+       /**
+        * Check whether this item has a potential conflict with the given item
+        *
+        * This checks whether the given item is in the list of conflicts of
+        * the current item, but makes no judgment about whether the conflict
+        * is currently at play (either one of the items may not be selected)
+        *
+        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
+        * @return {boolean} This item has a conflict with the given item
+        */
+       FilterItem.prototype.existsInConflicts = function ( filterItem ) {
+               return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
+       };
+
+       /**
+        * Set the state of this filter as being conflicted
+        * (This means any filters in its conflicts are selected)
+        *
+        * @param {boolean} [conflicted] Filter is in conflict state
+        * @fires update
+        */
+       FilterItem.prototype.toggleConflicted = function ( conflicted ) {
+               conflicted = conflicted === undefined ? !this.conflicted : conflicted;
+
+               if ( this.conflicted !== conflicted ) {
+                       this.conflicted = conflicted;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Set the state of this filter as being already included
+        * (This means any filters in its superset are selected)
+        *
+        * @param {boolean} [included] Filter is included as part of a subset
+        * @fires update
+        */
+       FilterItem.prototype.toggleIncluded = function ( included ) {
+               included = included === undefined ? !this.included : included;
+
+               if ( this.included !== included ) {
+                       this.included = included;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Toggle the fully covered state of the item
+        *
+        * @param {boolean} [isFullyCovered] Filter is fully covered
+        * @fires update
+        */
+       FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
+               isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
+
+               if ( this.fullyCovered !== isFullyCovered ) {
+                       this.fullyCovered = isFullyCovered;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Toggle the visibility of this item
+        *
+        * @param {boolean} [isVisible] Item is visible
+        */
+       FilterItem.prototype.toggleVisible = function ( isVisible ) {
+               isVisible = isVisible === undefined ? !this.visible : !!isVisible;
+
+               if ( this.visible !== isVisible ) {
+                       this.visible = isVisible;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Check whether the item is visible
+        *
+        * @return {boolean} Item is visible
+        */
+       FilterItem.prototype.isVisible = function () {
+               return this.visible;
+       };
+
+       module.exports = FilterItem;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/FiltersViewModel.js
new file mode 100644 (file)
index 0000000..d89bb28
--- /dev/null
@@ -0,0 +1,1302 @@
+( function () {
+       var FilterGroup = require( './FilterGroup.js' ),
+               FilterItem = require( './FilterItem.js' ),
+               FiltersViewModel;
+
+       /**
+        * View model for the filters selection and display
+        *
+        * @class mw.rcfilters.dm.FiltersViewModel
+        * @mixins OO.EventEmitter
+        * @mixins OO.EmitterList
+        *
+        * @constructor
+        */
+       FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+               OO.EmitterList.call( this );
+
+               this.groups = {};
+               this.defaultParams = {};
+               this.highlightEnabled = false;
+               this.parameterMap = {};
+               this.emptyParameterState = null;
+
+               this.views = {};
+               this.currentView = 'default';
+               this.searchQuery = null;
+
+               // Events
+               this.aggregate( { update: 'filterItemUpdate' } );
+               this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
+       };
+
+       /* Initialization */
+       OO.initClass( FiltersViewModel );
+       OO.mixinClass( FiltersViewModel, OO.EventEmitter );
+       OO.mixinClass( FiltersViewModel, OO.EmitterList );
+
+       /* Events */
+
+       /**
+        * @event initialize
+        *
+        * Filter list is initialized
+        */
+
+       /**
+        * @event update
+        *
+        * Model has been updated
+        */
+
+       /**
+        * @event itemUpdate
+        * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
+        *
+        * Filter item has changed
+        */
+
+       /**
+        * @event highlightChange
+        * @param {boolean} Highlight feature is enabled
+        *
+        * Highlight feature has been toggled enabled or disabled
+        */
+
+       /* Methods */
+
+       /**
+        * Re-assess the states of filter items based on the interactions between them
+        *
+        * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
+        *  method will go over the state of all items
+        */
+       FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
+               var allSelected,
+                       model = this,
+                       iterationItems = item !== undefined ? [ item ] : this.getItems();
+
+               iterationItems.forEach( function ( checkedItem ) {
+                       var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
+                               groupModel = checkedItem.getGroupModel();
+
+                       // Check for subsets (included filters) plus the item itself:
+                       allCheckedItems.forEach( function ( filterItemName ) {
+                               var itemInSubset = model.getItemByName( filterItemName );
+
+                               itemInSubset.toggleIncluded(
+                                       // If any of itemInSubset's supersets are selected, this item
+                                       // is included
+                                       itemInSubset.getSuperset().some( function ( supersetName ) {
+                                               return ( model.getItemByName( supersetName ).isSelected() );
+                                       } )
+                               );
+                       } );
+
+                       // Update coverage for the changed group
+                       if ( groupModel.isFullCoverage() ) {
+                               allSelected = groupModel.areAllSelected();
+                               groupModel.getItems().forEach( function ( filterItem ) {
+                                       filterItem.toggleFullyCovered( allSelected );
+                               } );
+                       }
+               } );
+
+               // Check for conflicts
+               // In this case, we must go over all items, since
+               // conflicts are bidirectional and depend not only on
+               // individual items, but also on the selected states of
+               // the groups they're in.
+               this.getItems().forEach( function ( filterItem ) {
+                       var inConflict = false,
+                               filterItemGroup = filterItem.getGroupModel();
+
+                       // For each item, see if that item is still conflicting
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( model.groups, function ( groupName, groupModel ) {
+                               if ( filterItem.getGroupName() === groupName ) {
+                                       // Check inside the group
+                                       inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
+                               } else {
+                                       // According to the spec, if two items conflict from two different
+                                       // groups, the conflict only lasts if the groups **only have selected
+                                       // items that are conflicting**. If a group has selected items that
+                                       // are conflicting and non-conflicting, the scope of the result has
+                                       // expanded enough to completely remove the conflict.
+
+                                       // For example, see two groups with conflicts:
+                                       // userExpLevel: [
+                                       //   {
+                                       //     name: 'experienced',
+                                       //     conflicts: [ 'unregistered' ]
+                                       //   }
+                                       // ],
+                                       // registration: [
+                                       //   {
+                                       //     name: 'registered',
+                                       //   },
+                                       //   {
+                                       //     name: 'unregistered',
+                                       //   }
+                                       // ]
+                                       // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
+                                       // because, inherently, 'experienced' filter only includes registered users, and so
+                                       // both filters are in conflict with one another.
+                                       // However, the minute we select 'registered', the scope of our results
+                                       // has expanded to no longer have a conflict with 'experienced' filter, and
+                                       // so the conflict is removed.
+
+                                       // In our case, we need to check if the entire group conflicts with
+                                       // the entire item's group, so we follow the above spec
+                                       inConflict = (
+                                               // The foreign group is in conflict with this item
+                                               groupModel.areAllSelectedInConflictWith( filterItem ) &&
+                                               // Every selected member of the item's own group is also
+                                               // in conflict with the other group
+                                               filterItemGroup.findSelectedItems().every( function ( otherGroupItem ) {
+                                                       return groupModel.areAllSelectedInConflictWith( otherGroupItem );
+                                               } )
+                                       );
+                               }
+
+                               // If we're in conflict, this will return 'false' which
+                               // will break the loop. Otherwise, we're not in conflict
+                               // and the loop continues
+                               return !inConflict;
+                       } );
+
+                       // Toggle the item state
+                       filterItem.toggleConflicted( inConflict );
+               } );
+       };
+
+       /**
+        * Get whether the model has any conflict in its items
+        *
+        * @return {boolean} There is a conflict
+        */
+       FiltersViewModel.prototype.hasConflict = function () {
+               return this.getItems().some( function ( filterItem ) {
+                       return filterItem.isSelected() && filterItem.isConflicted();
+               } );
+       };
+
+       /**
+        * Get the first item with a current conflict
+        *
+        * @return {mw.rcfilters.dm.FilterItem} Conflicted item
+        */
+       FiltersViewModel.prototype.getFirstConflictedItem = function () {
+               var conflictedItem;
+
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( filterItem.isSelected() && filterItem.isConflicted() ) {
+                               conflictedItem = filterItem;
+                               return false;
+                       }
+               } );
+
+               return conflictedItem;
+       };
+
+       /**
+        * Set filters and preserve a group relationship based on
+        * the definition given by an object
+        *
+        * @param {Array} filterGroups Filters definition
+        * @param {Object} [views] Extra views definition
+        *  Expected in the following format:
+        *  {
+        *     namespaces: {
+        *       label: 'namespaces', // Message key
+        *       trigger: ':',
+        *       groups: [
+        *         {
+        *            // Group info
+        *            name: 'namespaces' // Parameter name
+        *            title: 'namespaces' // Message key
+        *            type: 'string_options',
+        *            separator: ';',
+        *            labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+        *            fullCoverage: true
+        *            items: []
+        *         }
+        *       ]
+        *     }
+        *  }
+        */
+       FiltersViewModel.prototype.initializeFilters = function ( filterGroups, views ) {
+               var filterConflictResult, groupConflictResult,
+                       allViews = {},
+                       model = this,
+                       items = [],
+                       groupConflictMap = {},
+                       filterConflictMap = {},
+                       /*!
+                        * Expand a conflict definition from group name to
+                        * the list of all included filters in that group.
+                        * We do this so that the direct relationship in the
+                        * models are consistently item->items rather than
+                        * mixing item->group with item->item.
+                        *
+                        * @param {Object} obj Conflict definition
+                        * @return {Object} Expanded conflict definition
+                        */
+                       expandConflictDefinitions = function ( obj ) {
+                               var result = {};
+
+                               // eslint-disable-next-line jquery/no-each-util
+                               $.each( obj, function ( key, conflicts ) {
+                                       var filterName,
+                                               adjustedConflicts = {};
+
+                                       conflicts.forEach( function ( conflict ) {
+                                               var filter;
+
+                                               if ( conflict.filter ) {
+                                                       filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
+                                                       filter = model.getItemByName( filterName );
+
+                                                       // Rename
+                                                       adjustedConflicts[ filterName ] = $.extend(
+                                                               {},
+                                                               conflict,
+                                                               {
+                                                                       filter: filterName,
+                                                                       item: filter
+                                                               }
+                                                       );
+                                               } else {
+                                                       // This conflict is for an entire group. Split it up to
+                                                       // represent each filter
+
+                                                       // Get the relevant group items
+                                                       model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
+                                                               // Rebuild the conflict
+                                                               adjustedConflicts[ groupItem.getName() ] = $.extend(
+                                                                       {},
+                                                                       conflict,
+                                                                       {
+                                                                               filter: groupItem.getName(),
+                                                                               item: groupItem
+                                                                       }
+                                                               );
+                                                       } );
+                                               }
+                                       } );
+
+                                       result[ key ] = adjustedConflicts;
+                               } );
+
+                               return result;
+                       };
+
+               // Reset
+               this.clearItems();
+               this.groups = {};
+               this.views = {};
+
+               // Clone
+               filterGroups = OO.copy( filterGroups );
+
+               // Normalize definition from the server
+               filterGroups.forEach( function ( data ) {
+                       var i;
+                       // What's this information needs to be normalized
+                       data.whatsThis = {
+                               body: data.whatsThisBody,
+                               header: data.whatsThisHeader,
+                               linkText: data.whatsThisLinkText,
+                               url: data.whatsThisUrl
+                       };
+
+                       // Title is a msg-key
+                       data.title = data.title ? mw.msg( data.title ) : data.name;
+
+                       // Filters are given to us with msg-keys, we need
+                       // to translate those before we hand them off
+                       for ( i = 0; i < data.filters.length; i++ ) {
+                               data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
+                               data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
+                       }
+               } );
+
+               // Collect views
+               allViews = $.extend( true, {
+                       default: {
+                               title: mw.msg( 'rcfilters-filterlist-title' ),
+                               groups: filterGroups
+                       }
+               }, views );
+
+               // Go over all views
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( allViews, function ( viewName, viewData ) {
+                       // Define the view
+                       model.views[ viewName ] = {
+                               name: viewData.name,
+                               title: viewData.title,
+                               trigger: viewData.trigger
+                       };
+
+                       // Go over groups
+                       viewData.groups.forEach( function ( groupData ) {
+                               var group = groupData.name;
+
+                               if ( !model.groups[ group ] ) {
+                                       model.groups[ group ] = new FilterGroup(
+                                               group,
+                                               $.extend( true, {}, groupData, { view: viewName } )
+                                       );
+                               }
+
+                               model.groups[ group ].initializeFilters( groupData.filters, groupData.default );
+                               items = items.concat( model.groups[ group ].getItems() );
+
+                               // Prepare conflicts
+                               if ( groupData.conflicts ) {
+                                       // Group conflicts
+                                       groupConflictMap[ group ] = groupData.conflicts;
+                               }
+
+                               groupData.filters.forEach( function ( itemData ) {
+                                       var filterItem = model.groups[ group ].getItemByParamName( itemData.name );
+                                       // Filter conflicts
+                                       if ( itemData.conflicts ) {
+                                               filterConflictMap[ filterItem.getName() ] = itemData.conflicts;
+                                       }
+                               } );
+                       } );
+               } );
+
+               // Add item references to the model, for lookup
+               this.addItems( items );
+
+               // Expand conflicts
+               groupConflictResult = expandConflictDefinitions( groupConflictMap );
+               filterConflictResult = expandConflictDefinitions( filterConflictMap );
+
+               // Set conflicts for groups
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( groupConflictResult, function ( group, conflicts ) {
+                       model.groups[ group ].setConflicts( conflicts );
+               } );
+
+               // Set conflicts for items
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( filterConflictResult, function ( filterName, conflicts ) {
+                       var filterItem = model.getItemByName( filterName );
+                       // set conflicts for items in the group
+                       filterItem.setConflicts( conflicts );
+               } );
+
+               // Create a map between known parameters and their models
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.groups, function ( group, groupModel ) {
+                       if (
+                               groupModel.getType() === 'send_unselected_if_any' ||
+                               groupModel.getType() === 'boolean' ||
+                               groupModel.getType() === 'any_value'
+                       ) {
+                               // Individual filters
+                               groupModel.getItems().forEach( function ( filterItem ) {
+                                       model.parameterMap[ filterItem.getParamName() ] = filterItem;
+                               } );
+                       } else if (
+                               groupModel.getType() === 'string_options' ||
+                               groupModel.getType() === 'single_option'
+                       ) {
+                               // Group
+                               model.parameterMap[ groupModel.getName() ] = groupModel;
+                       }
+               } );
+
+               this.setSearch( '' );
+
+               this.updateHighlightedState();
+
+               // Finish initialization
+               this.emit( 'initialize' );
+       };
+
+       /**
+        * Update filter view model state based on a parameter object
+        *
+        * @param {Object} params Parameters object
+        */
+       FiltersViewModel.prototype.updateStateFromParams = function ( params ) {
+               var filtersValue;
+               // For arbitrary numeric single_option values make sure the values
+               // are normalized to fit within the limits
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+                       params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] );
+               } );
+
+               // Update filter values
+               filtersValue = this.getFiltersFromParameters( params );
+               Object.keys( filtersValue ).forEach( function ( filterName ) {
+                       this.getItemByName( filterName ).setValue( filtersValue[ filterName ] );
+               }.bind( this ) );
+
+               // Update highlight state
+               this.getItemsSupportingHighlights().forEach( function ( filterItem ) {
+                       var color = params[ filterItem.getName() + '_color' ];
+                       if ( color ) {
+                               filterItem.setHighlightColor( color );
+                       } else {
+                               filterItem.clearHighlightColor();
+                       }
+               } );
+               this.updateHighlightedState();
+
+               // Check all filter interactions
+               this.reassessFilterInteractions();
+       };
+
+       /**
+        * Get a representation of an empty (falsey) parameter state
+        *
+        * @return {Object} Empty parameter state
+        */
+       FiltersViewModel.prototype.getEmptyParameterState = function () {
+               if ( !this.emptyParameterState ) {
+                       this.emptyParameterState = $.extend(
+                               true,
+                               {},
+                               this.getParametersFromFilters( {} ),
+                               this.getEmptyHighlightParameters()
+                       );
+               }
+               return this.emptyParameterState;
+       };
+
+       /**
+        * Get a representation of only the non-falsey parameters
+        *
+        * @param {Object} [parameters] A given parameter state to minimize. If not given the current
+        *  state of the system will be used.
+        * @return {Object} Empty parameter state
+        */
+       FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) {
+               var result = {};
+
+               parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
+
+               // Params
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.getEmptyParameterState(), function ( param, value ) {
+                       if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) {
+                               result[ param ] = parameters[ param ];
+                       }
+               } );
+
+               // Highlights
+               Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) {
+                       if ( parameters[ param ] ) {
+                               // If a highlight parameter is not undefined and not null
+                               // add it to the result
+                               result[ param ] = parameters[ param ];
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get a representation of the full parameter list, including all base values
+        *
+        * @return {Object} Full parameter representation
+        */
+       FiltersViewModel.prototype.getExpandedParamRepresentation = function () {
+               return $.extend(
+                       true,
+                       {},
+                       this.getEmptyParameterState(),
+                       this.getCurrentParameterState()
+               );
+       };
+
+       /**
+        * Get a parameter representation of the current state of the model
+        *
+        * @param {boolean} [removeStickyParams] Remove sticky filters from final result
+        * @return {Object} Parameter representation of the current state of the model
+        */
+       FiltersViewModel.prototype.getCurrentParameterState = function ( removeStickyParams ) {
+               var state = this.getMinimizedParamRepresentation( $.extend(
+                       true,
+                       {},
+                       this.getParametersFromFilters( this.getSelectedState() ),
+                       this.getHighlightParameters()
+               ) );
+
+               if ( removeStickyParams ) {
+                       state = this.removeStickyParams( state );
+               }
+
+               return state;
+       };
+
+       /**
+        * Delete sticky parameters from given object.
+        *
+        * @param {Object} paramState Parameter state
+        * @return {Object} Parameter state without sticky parameters
+        */
+       FiltersViewModel.prototype.removeStickyParams = function ( paramState ) {
+               this.getStickyParams().forEach( function ( paramName ) {
+                       delete paramState[ paramName ];
+               } );
+
+               return paramState;
+       };
+
+       /**
+        * Turn the highlight feature on or off
+        */
+       FiltersViewModel.prototype.updateHighlightedState = function () {
+               this.toggleHighlight( this.getHighlightedItems().length > 0 );
+       };
+
+       /**
+        * Get the object that defines groups by their name.
+        *
+        * @return {Object} Filter groups
+        */
+       FiltersViewModel.prototype.getFilterGroups = function () {
+               return this.groups;
+       };
+
+       /**
+        * Get the object that defines groups that match a certain view by their name.
+        *
+        * @param {string} [view] Requested view. If not given, uses current view
+        * @return {Object} Filter groups matching a display group
+        */
+       FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
+               var result = {};
+
+               view = view || this.getCurrentView();
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.groups, function ( groupName, groupModel ) {
+                       if ( groupModel.getView() === view ) {
+                               result[ groupName ] = groupModel;
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get an array of filters matching the given display group.
+        *
+        * @param {string} [view] Requested view. If not given, uses current view
+        * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
+        */
+       FiltersViewModel.prototype.getFiltersByView = function ( view ) {
+               var groups,
+                       result = [];
+
+               view = view || this.getCurrentView();
+
+               groups = this.getFilterGroupsByView( view );
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( groups, function ( groupName, groupModel ) {
+                       result = result.concat( groupModel.getItems() );
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get the trigger for the requested view.
+        *
+        * @param {string} view View name
+        * @return {string} View trigger, if exists
+        */
+       FiltersViewModel.prototype.getViewTrigger = function ( view ) {
+               return ( this.views[ view ] && this.views[ view ].trigger ) || '';
+       };
+
+       /**
+        * Get the value of a specific parameter
+        *
+        * @param {string} name Parameter name
+        * @return {number|string} Parameter value
+        */
+       FiltersViewModel.prototype.getParamValue = function ( name ) {
+               return this.parameters[ name ];
+       };
+
+       /**
+        * Get the current selected state of the filters
+        *
+        * @param {boolean} [onlySelected] return an object containing only the filters with a value
+        * @return {Object} Filters selected state
+        */
+       FiltersViewModel.prototype.getSelectedState = function ( onlySelected ) {
+               var i,
+                       items = this.getItems(),
+                       result = {};
+
+               for ( i = 0; i < items.length; i++ ) {
+                       if ( !onlySelected || items[ i ].getValue() ) {
+                               result[ items[ i ].getName() ] = items[ i ].getValue();
+                       }
+               }
+
+               return result;
+       };
+
+       /**
+        * Get the current full state of the filters
+        *
+        * @return {Object} Filters full state
+        */
+       FiltersViewModel.prototype.getFullState = function () {
+               var i,
+                       items = this.getItems(),
+                       result = {};
+
+               for ( i = 0; i < items.length; i++ ) {
+                       result[ items[ i ].getName() ] = {
+                               selected: items[ i ].isSelected(),
+                               conflicted: items[ i ].isConflicted(),
+                               included: items[ i ].isIncluded()
+                       };
+               }
+
+               return result;
+       };
+
+       /**
+        * Get an object representing default parameters state
+        *
+        * @return {Object} Default parameter values
+        */
+       FiltersViewModel.prototype.getDefaultParams = function () {
+               var result = {};
+
+               // Get default filter state
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.groups, function ( name, model ) {
+                       if ( !model.isSticky() ) {
+                               $.extend( true, result, model.getDefaultParams() );
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get a parameter representation of all sticky parameters
+        *
+        * @return {Object} Sticky parameter values
+        */
+       FiltersViewModel.prototype.getStickyParams = function () {
+               var result = [];
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.groups, function ( name, model ) {
+                       if ( model.isSticky() ) {
+                               if ( model.isPerGroupRequestParameter() ) {
+                                       result.push( name );
+                               } else {
+                                       // Each filter is its own param
+                                       result = result.concat( model.getItems().map( function ( filterItem ) {
+                                               return filterItem.getParamName();
+                                       } ) );
+                               }
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get a parameter representation of all sticky parameters
+        *
+        * @return {Object} Sticky parameter values
+        */
+       FiltersViewModel.prototype.getStickyParamsValues = function () {
+               var result = {};
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.groups, function ( name, model ) {
+                       if ( model.isSticky() ) {
+                               $.extend( true, result, model.getParamRepresentation() );
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Analyze the groups and their filters and output an object representing
+        * the state of the parameters they represent.
+        *
+        * @param {Object} [filterDefinition] An object defining the filter values,
+        *  keyed by filter names.
+        * @return {Object} Parameter state object
+        */
+       FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
+               var groupItemDefinition,
+                       result = {},
+                       groupItems = this.getFilterGroups();
+
+               if ( filterDefinition ) {
+                       groupItemDefinition = {};
+                       // Filter definition is "flat", but in effect
+                       // each group needs to tell us its result based
+                       // on the values in it. We need to split this list
+                       // back into groupings so we can "feed" it to the
+                       // loop below, and we need to expand it so it includes
+                       // all filters (set to false)
+                       this.getItems().forEach( function ( filterItem ) {
+                               groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
+                               groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = filterItem.coerceValue( filterDefinition[ filterItem.getName() ] );
+                       } );
+               }
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( groupItems, function ( group, model ) {
+                       $.extend(
+                               result,
+                               model.getParamRepresentation(
+                                       groupItemDefinition ?
+                                               groupItemDefinition[ group ] : null
+                               )
+                       );
+               } );
+
+               return result;
+       };
+
+       /**
+        * This is the opposite of the #getParametersFromFilters method; this goes over
+        * the given parameters and translates into a selected/unselected value in the filters.
+        *
+        * @param {Object} params Parameters query object
+        * @return {Object} Filter state object
+        */
+       FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
+               var groupMap = {},
+                       model = this,
+                       result = {};
+
+               // Go over the given parameters, break apart to groupings
+               // The resulting object represents the group with its parameter
+               // values. For example:
+               // {
+               //    group1: {
+               //       param1: "1",
+               //       param2: "0",
+               //       param3: "1"
+               //    },
+               //    group2: "param4|param5"
+               // }
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( params, function ( paramName, paramValue ) {
+                       var groupName,
+                               itemOrGroup = model.parameterMap[ paramName ];
+
+                       if ( itemOrGroup ) {
+                               groupName = itemOrGroup instanceof FilterItem ?
+                                       itemOrGroup.getGroupName() : itemOrGroup.getName();
+
+                               groupMap[ groupName ] = groupMap[ groupName ] || {};
+                               groupMap[ groupName ][ paramName ] = paramValue;
+                       }
+               } );
+
+               // Go over all groups, so we make sure we get the complete output
+               // even if the parameters don't include a certain group
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.groups, function ( groupName, groupModel ) {
+                       result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get the highlight parameters based on current filter configuration
+        *
+        * @return {Object} Object where keys are `<filter name>_color` and values
+        *                  are the selected highlight colors.
+        */
+       FiltersViewModel.prototype.getHighlightParameters = function () {
+               var highlightEnabled = this.isHighlightEnabled(),
+                       result = {};
+
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( filterItem.isHighlightSupported() ) {
+                               result[ filterItem.getName() + '_color' ] = highlightEnabled && filterItem.isHighlighted() ?
+                                       filterItem.getHighlightColor() :
+                                       null;
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get an object representing the complete empty state of highlights
+        *
+        * @return {Object} Object containing all the highlight parameters set to their negative value
+        */
+       FiltersViewModel.prototype.getEmptyHighlightParameters = function () {
+               var result = {};
+
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( filterItem.isHighlightSupported() ) {
+                               result[ filterItem.getName() + '_color' ] = null;
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Get an array of currently applied highlight colors
+        *
+        * @return {string[]} Currently applied highlight colors
+        */
+       FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
+               var result = [];
+
+               if ( this.isHighlightEnabled() ) {
+                       this.getHighlightedItems().forEach( function ( filterItem ) {
+                               var color = filterItem.getHighlightColor();
+
+                               if ( result.indexOf( color ) === -1 ) {
+                                       result.push( color );
+                               }
+                       } );
+               }
+
+               return result;
+       };
+
+       /**
+        * Sanitize value group of a string_option groups type
+        * Remove duplicates and make sure to only use valid
+        * values.
+        *
+        * @private
+        * @param {string} groupName Group name
+        * @param {string[]} valueArray Array of values
+        * @return {string[]} Array of valid values
+        */
+       FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
+               var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
+                       return filterItem.getParamName();
+               } );
+
+               return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
+       };
+
+       /**
+        * Check whether no visible filter is selected.
+        *
+        * Filter groups that are hidden or sticky are not shown in the
+        * active filters area and therefore not included in this check.
+        *
+        * @return {boolean} No visible filter is selected
+        */
+       FiltersViewModel.prototype.areVisibleFiltersEmpty = function () {
+               // Check if there are either any selected items or any items
+               // that have highlight enabled
+               return !this.getItems().some( function ( filterItem ) {
+                       var visible = !filterItem.getGroupModel().isSticky() && !filterItem.getGroupModel().isHidden(),
+                               active = ( filterItem.isSelected() || filterItem.isHighlighted() );
+                       return visible && active;
+               } );
+       };
+
+       /**
+        * Check whether the invert state is a valid one. A valid invert state is one where
+        * there are actual namespaces selected.
+        *
+        * This is done to compare states to previous ones that may have had the invert model
+        * selected but effectively had no namespaces, so are not effectively different than
+        * ones where invert is not selected.
+        *
+        * @return {boolean} Invert is effectively selected
+        */
+       FiltersViewModel.prototype.areNamespacesEffectivelyInverted = function () {
+               return this.getInvertModel().isSelected() &&
+                       this.findSelectedItems().some( function ( itemModel ) {
+                               return itemModel.getGroupModel().getName() === 'namespace';
+                       } );
+       };
+
+       /**
+        * Get the item that matches the given name
+        *
+        * @param {string} name Filter name
+        * @return {mw.rcfilters.dm.FilterItem} Filter item
+        */
+       FiltersViewModel.prototype.getItemByName = function ( name ) {
+               return this.getItems().filter( function ( item ) {
+                       return name === item.getName();
+               } )[ 0 ];
+       };
+
+       /**
+        * Set all filters to false or empty/all
+        * This is equivalent to display all.
+        */
+       FiltersViewModel.prototype.emptyAllFilters = function () {
+               this.getItems().forEach( function ( filterItem ) {
+                       if ( !filterItem.getGroupModel().isSticky() ) {
+                               this.toggleFilterSelected( filterItem.getName(), false );
+                       }
+               }.bind( this ) );
+       };
+
+       /**
+        * Toggle selected state of one item
+        *
+        * @param {string} name Name of the filter item
+        * @param {boolean} [isSelected] Filter selected state
+        */
+       FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
+               var item = this.getItemByName( name );
+
+               if ( item ) {
+                       item.toggleSelected( isSelected );
+               }
+       };
+
+       /**
+        * Toggle selected state of items by their names
+        *
+        * @param {Object} filterDef Filter definitions
+        */
+       FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
+               Object.keys( filterDef ).forEach( function ( name ) {
+                       this.toggleFilterSelected( name, filterDef[ name ] );
+               }.bind( this ) );
+       };
+
+       /**
+        * Get a group model from its name
+        *
+        * @param {string} groupName Group name
+        * @return {mw.rcfilters.dm.FilterGroup} Group model
+        */
+       FiltersViewModel.prototype.getGroup = function ( groupName ) {
+               return this.groups[ groupName ];
+       };
+
+       /**
+        * Get all filters within a specified group by its name
+        *
+        * @param {string} groupName Group name
+        * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
+        */
+       FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
+               return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
+       };
+
+       /**
+        * Find items whose labels match the given string
+        *
+        * @param {string} query Search string
+        * @param {boolean} [returnFlat] Return a flat array. If false, the result
+        *  is an object whose keys are the group names and values are an array of
+        *  filters per group. If set to true, returns an array of filters regardless
+        *  of their groups.
+        * @return {Object} An object of items to show
+        *  arranged by their group names
+        */
+       FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
+               var i, searchIsEmpty,
+                       groupTitle,
+                       result = {},
+                       flatResult = [],
+                       view = this.getViewByTrigger( query.substr( 0, 1 ) ),
+                       items = this.getFiltersByView( view );
+
+               // Normalize so we can search strings regardless of case and view
+               query = query.trim().toLowerCase();
+               if ( view !== 'default' ) {
+                       query = query.substr( 1 );
+               }
+               // Trim again to also intercept cases where the spaces were after the trigger
+               // eg: '#   str'
+               query = query.trim();
+
+               // Check if the search if actually empty; this can be a problem when
+               // we use prefixes to denote different views
+               searchIsEmpty = query.length === 0;
+
+               // item label starting with the query string
+               for ( i = 0; i < items.length; i++ ) {
+                       if (
+                               searchIsEmpty ||
+                               items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
+                               (
+                                       // For tags, we want the parameter name to be included in the search
+                                       view === 'tags' &&
+                                       items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+                               )
+                       ) {
+                               result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+                               result[ items[ i ].getGroupName() ].push( items[ i ] );
+                               flatResult.push( items[ i ] );
+                       }
+               }
+
+               if ( $.isEmptyObject( result ) ) {
+                       // item containing the query string in their label, description, or group title
+                       for ( i = 0; i < items.length; i++ ) {
+                               groupTitle = items[ i ].getGroupModel().getTitle();
+                               if (
+                                       searchIsEmpty ||
+                                       items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
+                                       items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
+                                       groupTitle.toLowerCase().indexOf( query ) > -1 ||
+                                       (
+                                               // For tags, we want the parameter name to be included in the search
+                                               view === 'tags' &&
+                                               items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+                                       )
+                               ) {
+                                       result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
+                                       result[ items[ i ].getGroupName() ].push( items[ i ] );
+                                       flatResult.push( items[ i ] );
+                               }
+                       }
+               }
+
+               return returnFlat ? flatResult : result;
+       };
+
+       /**
+        * Get items that are highlighted
+        *
+        * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
+        */
+       FiltersViewModel.prototype.getHighlightedItems = function () {
+               return this.getItems().filter( function ( filterItem ) {
+                       return filterItem.isHighlightSupported() &&
+                               filterItem.getHighlightColor();
+               } );
+       };
+
+       /**
+        * Get items that allow highlights even if they're not currently highlighted
+        *
+        * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
+        */
+       FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
+               return this.getItems().filter( function ( filterItem ) {
+                       return filterItem.isHighlightSupported();
+               } );
+       };
+
+       /**
+        * Get all selected items
+        *
+        * @return {mw.rcfilters.dm.FilterItem[]} Selected items
+        */
+       FiltersViewModel.prototype.findSelectedItems = function () {
+               var allSelected = [];
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+                       allSelected = allSelected.concat( groupModel.findSelectedItems() );
+               } );
+
+               return allSelected;
+       };
+
+       /**
+        * Get the current view
+        *
+        * @return {string} Current view
+        */
+       FiltersViewModel.prototype.getCurrentView = function () {
+               return this.currentView;
+       };
+
+       /**
+        * Get the label for the current view
+        *
+        * @param {string} viewName View name
+        * @return {string} Label for the current view
+        */
+       FiltersViewModel.prototype.getViewTitle = function ( viewName ) {
+               viewName = viewName || this.getCurrentView();
+
+               return this.views[ viewName ] && this.views[ viewName ].title;
+       };
+
+       /**
+        * Get the view that fits the given trigger
+        *
+        * @param {string} trigger Trigger
+        * @return {string} Name of view
+        */
+       FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
+               var result = 'default';
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( this.views, function ( name, data ) {
+                       if ( data.trigger === trigger ) {
+                               result = name;
+                       }
+               } );
+
+               return result;
+       };
+
+       /**
+        * Return a version of the given string that is without any
+        * view triggers.
+        *
+        * @param {string} str Given string
+        * @return {string} Result
+        */
+       FiltersViewModel.prototype.removeViewTriggers = function ( str ) {
+               if ( this.getViewFromString( str ) !== 'default' ) {
+                       str = str.substr( 1 );
+               }
+
+               return str;
+       };
+
+       /**
+        * Get the view from the given string by a trigger, if it exists
+        *
+        * @param {string} str Given string
+        * @return {string} View name
+        */
+       FiltersViewModel.prototype.getViewFromString = function ( str ) {
+               return this.getViewByTrigger( str.substr( 0, 1 ) );
+       };
+
+       /**
+        * Set the current search for the system.
+        * This also dictates what items and groups are visible according
+        * to the search in #findMatches
+        *
+        * @param {string} searchQuery Search query, including triggers
+        * @fires searchChange
+        */
+       FiltersViewModel.prototype.setSearch = function ( searchQuery ) {
+               var visibleGroups, visibleGroupNames;
+
+               if ( this.searchQuery !== searchQuery ) {
+                       // Check if the view changed
+                       this.switchView( this.getViewFromString( searchQuery ) );
+
+                       visibleGroups = this.findMatches( searchQuery );
+                       visibleGroupNames = Object.keys( visibleGroups );
+
+                       // Update visibility of items and groups
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+                               // Check if the group is visible at all
+                               groupModel.toggleVisible( visibleGroupNames.indexOf( groupName ) !== -1 );
+                               groupModel.setVisibleItems( visibleGroups[ groupName ] || [] );
+                       } );
+
+                       this.searchQuery = searchQuery;
+                       this.emit( 'searchChange', this.searchQuery );
+               }
+       };
+
+       /**
+        * Get the current search
+        *
+        * @return {string} Current search query
+        */
+       FiltersViewModel.prototype.getSearch = function () {
+               return this.searchQuery;
+       };
+
+       /**
+        * Switch the current view
+        *
+        * @private
+        * @param {string} view View name
+        */
+       FiltersViewModel.prototype.switchView = function ( view ) {
+               if ( this.views[ view ] && this.currentView !== view ) {
+                       this.currentView = view;
+               }
+       };
+
+       /**
+        * Toggle the highlight feature on and off.
+        * Propagate the change to filter items.
+        *
+        * @param {boolean} enable Highlight should be enabled
+        * @fires highlightChange
+        */
+       FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
+               enable = enable === undefined ? !this.highlightEnabled : enable;
+
+               if ( this.highlightEnabled !== enable ) {
+                       this.highlightEnabled = enable;
+                       this.emit( 'highlightChange', this.highlightEnabled );
+               }
+       };
+
+       /**
+        * Check if the highlight feature is enabled
+        * @return {boolean}
+        */
+       FiltersViewModel.prototype.isHighlightEnabled = function () {
+               return !!this.highlightEnabled;
+       };
+
+       /**
+        * Toggle the inverted namespaces property on and off.
+        * Propagate the change to namespace filter items.
+        *
+        * @param {boolean} enable Inverted property is enabled
+        */
+       FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
+               this.toggleFilterSelected( this.getInvertModel().getName(), enable );
+       };
+
+       /**
+        * Get the model object that represents the 'invert' filter
+        *
+        * @return {mw.rcfilters.dm.FilterItem}
+        */
+       FiltersViewModel.prototype.getInvertModel = function () {
+               return this.getGroup( 'invertGroup' ).getItemByParamName( 'invert' );
+       };
+
+       /**
+        * Set highlight color for a specific filter item
+        *
+        * @param {string} filterName Name of the filter item
+        * @param {string} color Selected color
+        */
+       FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
+               this.getItemByName( filterName ).setHighlightColor( color );
+       };
+
+       /**
+        * Clear highlight for a specific filter item
+        *
+        * @param {string} filterName Name of the filter item
+        */
+       FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
+               this.getItemByName( filterName ).clearHighlightColor();
+       };
+
+       module.exports = FiltersViewModel;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/ItemModel.js b/resources/src/mediawiki.rcfilters/dm/ItemModel.js
new file mode 100644 (file)
index 0000000..2dc578e
--- /dev/null
@@ -0,0 +1,276 @@
+( function () {
+       /**
+        * RCFilter base item model
+        *
+        * @class mw.rcfilters.dm.ItemModel
+        * @mixins OO.EventEmitter
+        *
+        * @constructor
+        * @param {string} param Filter param name
+        * @param {Object} config Configuration object
+        * @cfg {string} [label] The label for the filter
+        * @cfg {string} [description] The description of the filter
+        * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
+        *  group. If the prefix has 'invert' state, the parameter is expected to be an object
+        *  with 'default' and 'inverted' as keys.
+        * @cfg {boolean} [active=true] The filter is active and affecting the result
+        * @cfg {boolean} [selected] The item is selected
+        * @cfg {*} [value] The value of this item
+        * @cfg {string} [namePrefix='item_'] A prefix to add to the param name to act as a unique
+        *  identifier
+        * @cfg {string} [cssClass] The class identifying the results that match this filter
+        * @cfg {string[]} [identifiers] An array of identifiers for this item. They will be
+        *  added and considered in the view.
+        * @cfg {string} [defaultHighlightColor=null] If set, highlight this filter by default with this color
+        */
+       var ItemModel = function MwRcfiltersDmItemModel( param, config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+
+               this.param = param;
+               this.namePrefix = config.namePrefix || 'item_';
+               this.name = this.namePrefix + param;
+
+               this.label = config.label || this.name;
+               this.labelPrefixKey = config.labelPrefixKey;
+               this.description = config.description || '';
+               this.setValue( config.value || config.selected );
+
+               this.identifiers = config.identifiers || [];
+
+               // Highlight
+               this.cssClass = config.cssClass;
+               this.highlightColor = config.defaultHighlightColor || null;
+       };
+
+       /* Initialization */
+
+       OO.initClass( ItemModel );
+       OO.mixinClass( ItemModel, OO.EventEmitter );
+
+       /* Events */
+
+       /**
+        * @event update
+        *
+        * The state of this filter has changed
+        */
+
+       /* Methods */
+
+       /**
+        * Return the representation of the state of this item.
+        *
+        * @return {Object} State of the object
+        */
+       ItemModel.prototype.getState = function () {
+               return {
+                       selected: this.isSelected()
+               };
+       };
+
+       /**
+        * Get the name of this filter
+        *
+        * @return {string} Filter name
+        */
+       ItemModel.prototype.getName = function () {
+               return this.name;
+       };
+
+       /**
+        * Get the message key to use to wrap the label. This message takes the label as a parameter.
+        *
+        * @param {boolean} inverted Whether this item should be considered inverted
+        * @return {string|null} Message key, or null if no message
+        */
+       ItemModel.prototype.getLabelMessageKey = function ( inverted ) {
+               if ( this.labelPrefixKey ) {
+                       if ( typeof this.labelPrefixKey === 'string' ) {
+                               return this.labelPrefixKey;
+                       }
+                       return this.labelPrefixKey[
+                               // Only use inverted-prefix if the item is selected
+                               // Highlight-only an inverted item makes no sense
+                               inverted && this.isSelected() ?
+                                       'inverted' : 'default'
+                       ];
+               }
+               return null;
+       };
+
+       /**
+        * Get the param name or value of this filter
+        *
+        * @return {string} Filter param name
+        */
+       ItemModel.prototype.getParamName = function () {
+               return this.param;
+       };
+
+       /**
+        * Get the message representing the state of this model.
+        *
+        * @return {string} State message
+        */
+       ItemModel.prototype.getStateMessage = function () {
+               // Display description
+               return this.getDescription();
+       };
+
+       /**
+        * Get the label of this filter
+        *
+        * @return {string} Filter label
+        */
+       ItemModel.prototype.getLabel = function () {
+               return this.label;
+       };
+
+       /**
+        * Get the description of this filter
+        *
+        * @return {string} Filter description
+        */
+       ItemModel.prototype.getDescription = function () {
+               return this.description;
+       };
+
+       /**
+        * Get the default value of this filter
+        *
+        * @return {boolean} Filter default
+        */
+       ItemModel.prototype.getDefault = function () {
+               return this.default;
+       };
+
+       /**
+        * Get the selected state of this filter
+        *
+        * @return {boolean} Filter is selected
+        */
+       ItemModel.prototype.isSelected = function () {
+               return !!this.value;
+       };
+
+       /**
+        * Toggle the selected state of the item
+        *
+        * @param {boolean} [isSelected] Filter is selected
+        * @fires update
+        */
+       ItemModel.prototype.toggleSelected = function ( isSelected ) {
+               isSelected = isSelected === undefined ? !this.isSelected() : isSelected;
+               this.setValue( isSelected );
+       };
+
+       /**
+        * Get the value
+        *
+        * @return {*}
+        */
+       ItemModel.prototype.getValue = function () {
+               return this.value;
+       };
+
+       /**
+        * Convert a given value to the appropriate representation based on group type
+        *
+        * @param {*} value
+        * @return {*}
+        */
+       ItemModel.prototype.coerceValue = function ( value ) {
+               return this.getGroupModel().getType() === 'any_value' ? value : !!value;
+       };
+
+       /**
+        * Set the value
+        *
+        * @param {*} newValue
+        */
+       ItemModel.prototype.setValue = function ( newValue ) {
+               newValue = this.coerceValue( newValue );
+               if ( this.value !== newValue ) {
+                       this.value = newValue;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Set the highlight color
+        *
+        * @param {string|null} highlightColor
+        */
+       ItemModel.prototype.setHighlightColor = function ( highlightColor ) {
+               if ( !this.isHighlightSupported() ) {
+                       return;
+               }
+               // If the highlight color on the item and in the parameter is null/undefined, return early.
+               if ( !this.highlightColor && !highlightColor ) {
+                       return;
+               }
+
+               if ( this.highlightColor !== highlightColor ) {
+                       this.highlightColor = highlightColor;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Clear the highlight color
+        */
+       ItemModel.prototype.clearHighlightColor = function () {
+               this.setHighlightColor( null );
+       };
+
+       /**
+        * Get the highlight color, or null if none is configured
+        *
+        * @return {string|null}
+        */
+       ItemModel.prototype.getHighlightColor = function () {
+               return this.highlightColor;
+       };
+
+       /**
+        * Get the CSS class that matches changes that fit this filter
+        * or null if none is configured
+        *
+        * @return {string|null}
+        */
+       ItemModel.prototype.getCssClass = function () {
+               return this.cssClass;
+       };
+
+       /**
+        * Get the item's identifiers
+        *
+        * @return {string[]}
+        */
+       ItemModel.prototype.getIdentifiers = function () {
+               return this.identifiers;
+       };
+
+       /**
+        * Check if the highlight feature is supported for this filter
+        *
+        * @return {boolean}
+        */
+       ItemModel.prototype.isHighlightSupported = function () {
+               return !!this.getCssClass();
+       };
+
+       /**
+        * Check if the filter is currently highlighted
+        *
+        * @return {boolean}
+        */
+       ItemModel.prototype.isHighlighted = function () {
+               return !!this.getHighlightColor();
+       };
+
+       module.exports = ItemModel;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/SavedQueriesModel.js b/resources/src/mediawiki.rcfilters/dm/SavedQueriesModel.js
new file mode 100644 (file)
index 0000000..34c57dd
--- /dev/null
@@ -0,0 +1,415 @@
+( function () {
+       var SavedQueryItemModel = require( './SavedQueryItemModel.js' ),
+               SavedQueriesModel;
+
+       /**
+        * View model for saved queries
+        *
+        * @class mw.rcfilters.dm.SavedQueriesModel
+        * @mixins OO.EventEmitter
+        * @mixins OO.EmitterList
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
+        * @param {Object} [config] Configuration options
+        * @cfg {string} [default] Default query ID
+        */
+       SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+               OO.EmitterList.call( this );
+
+               this.default = config.default;
+               this.filtersModel = filtersModel;
+               this.converted = false;
+
+               // Events
+               this.aggregate( { update: 'itemUpdate' } );
+       };
+
+       /* Initialization */
+
+       OO.initClass( SavedQueriesModel );
+       OO.mixinClass( SavedQueriesModel, OO.EventEmitter );
+       OO.mixinClass( SavedQueriesModel, OO.EmitterList );
+
+       /* Events */
+
+       /**
+        * @event initialize
+        *
+        * Model is initialized
+        */
+
+       /**
+        * @event itemUpdate
+        * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
+        *
+        * An item has changed
+        */
+
+       /**
+        * @event default
+        * @param {string} New default ID
+        *
+        * The default has changed
+        */
+
+       /* Methods */
+
+       /**
+        * Initialize the saved queries model by reading it from the user's settings.
+        * The structure of the saved queries is:
+        * {
+        *    version: (string) Version number; if version 2, the query represents
+        *             parameters. Otherwise, the older version represented filters
+        *             and needs to be readjusted,
+        *    default: (string) Query ID
+        *    queries:{
+        *       query_id_1: {
+        *          data:{
+        *             filters: (Object) Minimal definition of the filters
+        *             highlights: (Object) Definition of the highlights
+        *          },
+        *          label: (optional) Name of this query
+        *       }
+        *    }
+        * }
+        *
+        * @param {Object} [savedQueries] An object with the saved queries with
+        *  the above structure.
+        * @fires initialize
+        */
+       SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
+               var model = this;
+
+               savedQueries = savedQueries || {};
+
+               this.clearItems();
+               this.default = null;
+               this.converted = false;
+
+               if ( savedQueries.version !== '2' ) {
+                       // Old version dealt with filter names. We need to migrate to the new structure
+                       // The new structure:
+                       // {
+                       //   version: (string) '2',
+                       //   default: (string) Query ID,
+                       //   queries: {
+                       //     query_id: {
+                       //       label: (string) Name of the query
+                       //       data: {
+                       //         params: (object) Representing all the parameter states
+                       //         highlights: (object) Representing all the filter highlight states
+                       //     }
+                       //   }
+                       // }
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( savedQueries.queries || {}, function ( id, obj ) {
+                               if ( obj.data && obj.data.filters ) {
+                                       obj.data = model.convertToParameters( obj.data );
+                               }
+                       } );
+
+                       this.converted = true;
+                       savedQueries.version = '2';
+               }
+
+               // Initialize the query items
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( savedQueries.queries || {}, function ( id, obj ) {
+                       var normalizedData = obj.data,
+                               isDefault = String( savedQueries.default ) === String( id );
+
+                       if ( normalizedData && normalizedData.params ) {
+                               // Backwards-compat fix: Remove sticky parameters from
+                               // the given data, if they exist
+                               normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );
+
+                               // Correct the invert state for effective selection
+                               if ( normalizedData.params.invert && !normalizedData.params.namespace ) {
+                                       delete normalizedData.params.invert;
+                               }
+
+                               model.cleanupHighlights( normalizedData );
+
+                               id = String( id );
+
+                               // Skip the addNewQuery method because we don't want to unnecessarily manipulate
+                               // the given saved queries unless we literally intend to (like in backwards compat fixes)
+                               // And the addNewQuery method also uses a minimization routine that checks for the
+                               // validity of items and minimizes the query. This isn't necessary for queries loaded
+                               // from the backend, and has the risk of removing values if they're temporarily
+                               // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
+                               model.addItems( [
+                                       new SavedQueryItemModel(
+                                               id,
+                                               obj.label,
+                                               normalizedData,
+                                               { default: isDefault }
+                                       )
+                               ] );
+
+                               if ( isDefault ) {
+                                       model.default = id;
+                               }
+                       }
+               } );
+
+               this.emit( 'initialize' );
+       };
+
+       /**
+        * Clean up highlight parameters.
+        * 'highlight' used to be stored, it's not inferred based on the presence of absence of
+        * filter colors.
+        *
+        * @param {Object} data Saved query data
+        */
+       SavedQueriesModel.prototype.cleanupHighlights = function ( data ) {
+               if (
+                       data.params.highlight === '0' &&
+                       data.highlights && Object.keys( data.highlights ).length
+               ) {
+                       data.highlights = {};
+               }
+               delete data.params.highlight;
+       };
+
+       /**
+        * Convert from representation of filters to representation of parameters
+        *
+        * @param {Object} data Query data
+        * @return {Object} New converted query data
+        */
+       SavedQueriesModel.prototype.convertToParameters = function ( data ) {
+               var newData = {},
+                       defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
+                       fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
+                       highlightEnabled = data.highlights.highlight;
+
+               delete data.highlights.highlight;
+
+               // Filters
+               newData.params = this.filtersModel.getMinimizedParamRepresentation(
+                       this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
+               );
+
+               // Highlights: appending _color to keys
+               newData.highlights = {};
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( data.highlights, function ( highlightedFilterName, value ) {
+                       if ( value ) {
+                               newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
+                       }
+               } );
+
+               // Add highlight
+               newData.params.highlight = String( Number( highlightEnabled || 0 ) );
+
+               return newData;
+       };
+
+       /**
+        * Add a query item
+        *
+        * @param {string} label Label for the new query
+        * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
+        * @param {boolean} isDefault Item is default
+        * @param {string} [id] Query ID, if exists. If this isn't given, a random
+        *  new ID will be created.
+        * @return {string} ID of the newly added query
+        */
+       SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
+               var normalizedData = { params: {}, highlights: {} },
+                       highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
+                       randomID = String( id || ( new Date() ).getTime() ),
+                       data = this.filtersModel.getMinimizedParamRepresentation( fulldata );
+
+               // Split highlight/params
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( data, function ( param, value ) {
+                       if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
+                               normalizedData.highlights[ param ] = value;
+                       } else {
+                               normalizedData.params[ param ] = value;
+                       }
+               } );
+
+               // Correct the invert state for effective selection
+               if ( normalizedData.params.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
+                       delete normalizedData.params.invert;
+               }
+
+               // Add item
+               this.addItems( [
+                       new SavedQueryItemModel(
+                               randomID,
+                               label,
+                               normalizedData,
+                               { default: isDefault }
+                       )
+               ] );
+
+               if ( isDefault ) {
+                       this.setDefault( randomID );
+               }
+
+               return randomID;
+       };
+
+       /**
+        * Remove query from model
+        *
+        * @param {string} queryID Query ID
+        */
+       SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
+               var query = this.getItemByID( queryID );
+
+               if ( query ) {
+                       // Check if this item was the default
+                       if ( String( this.getDefault() ) === String( queryID ) ) {
+                               // Nulify the default
+                               this.setDefault( null );
+                       }
+
+                       this.removeItems( [ query ] );
+               }
+       };
+
+       /**
+        * Get an item that matches the requested query
+        *
+        * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
+        * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
+        */
+       SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
+               // Minimize before comparison
+               fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );
+
+               // Correct the invert state for effective selection
+               if ( fullQueryComparison.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
+                       delete fullQueryComparison.invert;
+               }
+
+               return this.getItems().filter( function ( item ) {
+                       return OO.compare(
+                               item.getCombinedData(),
+                               fullQueryComparison
+                       );
+               } )[ 0 ];
+       };
+
+       /**
+        * Get query by its identifier
+        *
+        * @param {string} queryID Query identifier
+        * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
+        *  the search. Undefined if not found.
+        */
+       SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
+               return this.getItems().filter( function ( item ) {
+                       return item.getID() === queryID;
+               } )[ 0 ];
+       };
+
+       /**
+        * Get the full data representation of the default query, if it exists
+        *
+        * @return {Object|null} Representation of the default params if exists.
+        *  Null if default doesn't exist or if the user is not logged in.
+        */
+       SavedQueriesModel.prototype.getDefaultParams = function () {
+               return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
+       };
+
+       /**
+        * Get a full parameter representation of an item data
+        *
+        * @param  {Object} queryID Query ID
+        * @return {Object} Parameter representation
+        */
+       SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
+               var item = this.getItemByID( queryID ),
+                       data = item ? item.getData() : {};
+
+               return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
+       };
+
+       /**
+        * Build a full parameter representation given item data and model sticky values state
+        *
+        * @param  {Object} data Item data
+        * @return {Object} Full param representation
+        */
+       SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
+               data = data || {};
+               // Return parameter representation
+               return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
+                       data.params,
+                       data.highlights
+               ) );
+       };
+
+       /**
+        * Get the object representing the state of the entire model and items
+        *
+        * @return {Object} Object representing the state of the model and items
+        */
+       SavedQueriesModel.prototype.getState = function () {
+               var obj = { queries: {}, version: '2' };
+
+               // Translate the items to the saved object
+               this.getItems().forEach( function ( item ) {
+                       obj.queries[ item.getID() ] = item.getState();
+               } );
+
+               if ( this.getDefault() ) {
+                       obj.default = this.getDefault();
+               }
+
+               return obj;
+       };
+
+       /**
+        * Set a default query. Null to unset default.
+        *
+        * @param {string} itemID Query identifier
+        * @fires default
+        */
+       SavedQueriesModel.prototype.setDefault = function ( itemID ) {
+               if ( this.default !== itemID ) {
+                       this.default = itemID;
+
+                       // Set for individual itens
+                       this.getItems().forEach( function ( item ) {
+                               item.toggleDefault( item.getID() === itemID );
+                       } );
+
+                       this.emit( 'default', itemID );
+               }
+       };
+
+       /**
+        * Get the default query ID
+        *
+        * @return {string} Default query identifier
+        */
+       SavedQueriesModel.prototype.getDefault = function () {
+               return this.default;
+       };
+
+       /**
+        * Check if the saved queries were converted
+        *
+        * @return {boolean} Saved queries were converted from the previous
+        *  version to the new version
+        */
+       SavedQueriesModel.prototype.isConverted = function () {
+               return this.converted;
+       };
+
+       module.exports = SavedQueriesModel;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/SavedQueryItemModel.js b/resources/src/mediawiki.rcfilters/dm/SavedQueryItemModel.js
new file mode 100644 (file)
index 0000000..1774391
--- /dev/null
@@ -0,0 +1,127 @@
+( function () {
+       /**
+        * View model for a single saved query
+        *
+        * @class mw.rcfilters.dm.SavedQueryItemModel
+        * @mixins OO.EventEmitter
+        *
+        * @constructor
+        * @param {string} id Unique identifier
+        * @param {string} label Saved query label
+        * @param {Object} data Saved query data
+        * @param {Object} [config] Configuration options
+        * @cfg {boolean} [default] This item is the default
+        */
+       var SavedQueryItemModel = function MwRcfiltersDmSavedQueriesModel( id, label, data, config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this );
+
+               this.id = id;
+               this.label = label;
+               this.data = data;
+               this.default = !!config.default;
+       };
+
+       /* Initialization */
+
+       OO.initClass( SavedQueryItemModel );
+       OO.mixinClass( SavedQueryItemModel, OO.EventEmitter );
+
+       /* Events */
+
+       /**
+        * @event update
+        *
+        * Model has been updated
+        */
+
+       /* Methods */
+
+       /**
+        * Get an object representing the state of this item
+        *
+        * @return {Object} Object representing the current data state
+        *  of the object
+        */
+       SavedQueryItemModel.prototype.getState = function () {
+               return {
+                       data: this.getData(),
+                       label: this.getLabel()
+               };
+       };
+
+       /**
+        * Get the query's identifier
+        *
+        * @return {string} Query identifier
+        */
+       SavedQueryItemModel.prototype.getID = function () {
+               return this.id;
+       };
+
+       /**
+        * Get query label
+        *
+        * @return {string} Query label
+        */
+       SavedQueryItemModel.prototype.getLabel = function () {
+               return this.label;
+       };
+
+       /**
+        * Update the query label
+        *
+        * @param {string} newLabel New label
+        */
+       SavedQueryItemModel.prototype.updateLabel = function ( newLabel ) {
+               if ( newLabel && this.label !== newLabel ) {
+                       this.label = newLabel;
+                       this.emit( 'update' );
+               }
+       };
+
+       /**
+        * Get query data
+        *
+        * @return {Object} Object representing parameter and highlight data
+        */
+       SavedQueryItemModel.prototype.getData = function () {
+               return this.data;
+       };
+
+       /**
+        * Get the combined data of this item as a flat object of parameters
+        *
+        * @return {Object} Combined parameter data
+        */
+       SavedQueryItemModel.prototype.getCombinedData = function () {
+               return $.extend( true, {}, this.data.params, this.data.highlights );
+       };
+
+       /**
+        * Check whether this item is the default
+        *
+        * @return {boolean} Query is set to be default
+        */
+       SavedQueryItemModel.prototype.isDefault = function () {
+               return this.default;
+       };
+
+       /**
+        * Toggle the default state of this query item
+        *
+        * @param {boolean} isDefault Query is default
+        */
+       SavedQueryItemModel.prototype.toggleDefault = function ( isDefault ) {
+               isDefault = isDefault === undefined ? !this.default : isDefault;
+
+               if ( this.default !== isDefault ) {
+                       this.default = isDefault;
+                       this.emit( 'update' );
+               }
+       };
+
+       module.exports = SavedQueryItemModel;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js
deleted file mode 100644 (file)
index e51829c..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-( function () {
-       /**
-        * View model for the changes list
-        *
-        * @mixins OO.EventEmitter
-        *
-        * @param {jQuery} $initialFieldset The initial server-generated legacy form content
-        * @constructor
-        */
-       mw.rcfilters.dm.ChangesListViewModel = function MwRcfiltersDmChangesListViewModel( $initialFieldset ) {
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-
-               this.valid = true;
-               this.newChangesExist = false;
-               this.liveUpdate = false;
-               this.unseenWatchedChanges = false;
-
-               this.extractNextFrom( $initialFieldset );
-       };
-
-       /* Initialization */
-       OO.initClass( mw.rcfilters.dm.ChangesListViewModel );
-       OO.mixinClass( mw.rcfilters.dm.ChangesListViewModel, OO.EventEmitter );
-
-       /* Events */
-
-       /**
-        * @event invalidate
-        *
-        * The list of changes is now invalid (out of date)
-        */
-
-       /**
-        * @event update
-        * @param {jQuery|string} $changesListContent List of changes
-        * @param {jQuery} $fieldset Server-generated form
-        * @param {string} noResultsDetails Type of no result error
-        * @param {boolean} isInitialDOM Whether the previous dom variables are from the initial page load
-        * @param {boolean} fromLiveUpdate These are new changes fetched via Live Update
-        *
-        * The list of changes has been updated
-        */
-
-       /**
-        * @event newChangesExist
-        * @param {boolean} newChangesExist
-        *
-        * The existence of changes newer than those currently displayed has changed.
-        */
-
-       /**
-        * @event liveUpdateChange
-        * @param {boolean} enable
-        *
-        * The state of the 'live update' feature has changed.
-        */
-
-       /* Methods */
-
-       /**
-        * Invalidate the list of changes
-        *
-        * @fires invalidate
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.invalidate = function () {
-               if ( this.valid ) {
-                       this.valid = false;
-                       this.emit( 'invalidate' );
-               }
-       };
-
-       /**
-        * Update the model with an updated list of changes
-        *
-        * @param {jQuery|string} changesListContent
-        * @param {jQuery} $fieldset
-        * @param {string} noResultsDetails Type of no result error
-        * @param {boolean} [isInitialDOM] Using the initial (already attached) DOM elements
-        * @param {boolean} [separateOldAndNew] Whether a logical separation between old and new changes is needed
-        * @fires update
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ) {
-               var from = this.nextFrom;
-               this.valid = true;
-               this.extractNextFrom( $fieldset );
-               this.checkForUnseenWatchedChanges( changesListContent );
-               this.emit( 'update', changesListContent, $fieldset, noResultsDetails, isInitialDOM, separateOldAndNew ? from : null );
-       };
-
-       /**
-        * Specify whether new changes exist
-        *
-        * @param {boolean} newChangesExist
-        * @fires newChangesExist
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.setNewChangesExist = function ( newChangesExist ) {
-               if ( newChangesExist !== this.newChangesExist ) {
-                       this.newChangesExist = newChangesExist;
-                       this.emit( 'newChangesExist', newChangesExist );
-               }
-       };
-
-       /**
-        * @return {boolean} Whether new changes exist
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.getNewChangesExist = function () {
-               return this.newChangesExist;
-       };
-
-       /**
-        * Extract the value of the 'from' parameter from a link in the field set
-        *
-        * @param {jQuery} $fieldset
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.extractNextFrom = function ( $fieldset ) {
-               var data = $fieldset.find( '.rclistfrom > a, .wlinfo' ).data( 'params' );
-               if ( data && data.from ) {
-                       this.nextFrom = data.from;
-               }
-       };
-
-       /**
-        * @return {string} The 'from' parameter that can be used to query new changes
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.getNextFrom = function () {
-               return this.nextFrom;
-       };
-
-       /**
-        * Toggle the 'live update' feature on/off
-        *
-        * @param {boolean} enable
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.toggleLiveUpdate = function ( enable ) {
-               enable = enable === undefined ? !this.liveUpdate : enable;
-               if ( enable !== this.liveUpdate ) {
-                       this.liveUpdate = enable;
-                       this.emit( 'liveUpdateChange', this.liveUpdate );
-               }
-       };
-
-       /**
-        * @return {boolean} The 'live update' feature is enabled
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.getLiveUpdate = function () {
-               return this.liveUpdate;
-       };
-
-       /**
-        * Check if some of the given changes watched and unseen
-        *
-        * @param {jQuery|string} changeslistContent
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.checkForUnseenWatchedChanges = function ( changeslistContent ) {
-               this.unseenWatchedChanges = changeslistContent !== 'NO_RESULTS' &&
-                       changeslistContent.find( '.mw-changeslist-line-watched' ).length > 0;
-       };
-
-       /**
-        * @return {boolean} Whether some of the current changes are watched and unseen
-        */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.hasUnseenWatchedChanges = function () {
-               return this.unseenWatchedChanges;
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
deleted file mode 100644 (file)
index df2079e..0000000
+++ /dev/null
@@ -1,988 +0,0 @@
-( function () {
-       /**
-        * View model for a filter group
-        *
-        * @mixins OO.EventEmitter
-        * @mixins OO.EmitterList
-        *
-        * @constructor
-        * @param {string} name Group name
-        * @param {Object} [config] Configuration options
-        * @cfg {string} [type='send_unselected_if_any'] Group type
-        * @cfg {string} [view='default'] Name of the display group this group
-        *  is a part of.
-        * @cfg {boolean} [sticky] This group is 'sticky'. It is synchronized
-        *  with a preference, does not participate in Saved Queries, and is
-        *  not shown in the active filters area.
-        * @cfg {string} [title] Group title
-        * @cfg {boolean} [hidden] This group is hidden from the regular menu views
-        *  and the active filters area.
-        * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
-        *  group from the URL, even if it wasn't initially set up.
-        * @cfg {number} [range] An object defining minimum and maximum values for numeric
-        *  groups. { min: x, max: y }
-        * @cfg {number} [minValue] Minimum value for numeric groups
-        * @cfg {string} [separator='|'] Value separator for 'string_options' groups
-        * @cfg {boolean} [active] Group is active
-        * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
-        * @cfg {Object} [conflicts] Defines the conflicts for this filter group
-        * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
-        *  group. If the prefix has 'invert' state, the parameter is expected to be an object
-        *  with 'default' and 'inverted' as keys.
-        * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
-        * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
-        * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
-        * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
-        * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
-        * @cfg {boolean} [visible=true] The visibility of the group
-        */
-       mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
-               config = config || {};
-
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-               OO.EmitterList.call( this );
-
-               this.name = name;
-               this.type = config.type || 'send_unselected_if_any';
-               this.view = config.view || 'default';
-               this.sticky = !!config.sticky;
-               this.title = config.title || name;
-               this.hidden = !!config.hidden;
-               this.allowArbitrary = !!config.allowArbitrary;
-               this.numericRange = config.range;
-               this.separator = config.separator || '|';
-               this.labelPrefixKey = config.labelPrefixKey;
-               this.visible = config.visible === undefined ? true : !!config.visible;
-
-               this.currSelected = null;
-               this.active = !!config.active;
-               this.fullCoverage = !!config.fullCoverage;
-
-               this.whatsThis = config.whatsThis || {};
-
-               this.conflicts = config.conflicts || {};
-               this.defaultParams = {};
-               this.defaultFilters = {};
-
-               this.aggregate( { update: 'filterItemUpdate' } );
-               this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
-       };
-
-       /* Initialization */
-       OO.initClass( mw.rcfilters.dm.FilterGroup );
-       OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter );
-       OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList );
-
-       /* Events */
-
-       /**
-        * @event update
-        *
-        * Group state has been updated
-        */
-
-       /* Methods */
-
-       /**
-        * Initialize the group and create its filter items
-        *
-        * @param {Object} filterDefinition Filter definition for this group
-        * @param {string|Object} [groupDefault] Definition of the group default
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
-               var defaultParam,
-                       supersetMap = {},
-                       model = this,
-                       items = [];
-
-               filterDefinition.forEach( function ( filter ) {
-                       // Instantiate an item
-                       var subsetNames = [],
-                               filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
-                                       group: model.getName(),
-                                       label: filter.label || filter.name,
-                                       description: filter.description || '',
-                                       labelPrefixKey: model.labelPrefixKey,
-                                       cssClass: filter.cssClass,
-                                       identifiers: filter.identifiers,
-                                       defaultHighlightColor: filter.defaultHighlightColor
-                               } );
-
-                       if ( filter.subset ) {
-                               filter.subset = filter.subset.map( function ( el ) {
-                                       return el.filter;
-                               } );
-
-                               subsetNames = [];
-
-                               filter.subset.forEach( function ( subsetFilterName ) {
-                                       // Subsets (unlike conflicts) are always inside the same group
-                                       // We can re-map the names of the filters we are getting from
-                                       // the subsets with the group prefix
-                                       var subsetName = model.getPrefixedName( subsetFilterName );
-                                       // For convenience, we should store each filter's "supersets" -- these are
-                                       // the filters that have that item in their subset list. This will just
-                                       // make it easier to go through whether the item has any other items
-                                       // that affect it (and are selected) at any given time
-                                       supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
-                                       mw.rcfilters.utils.addArrayElementsUnique(
-                                               supersetMap[ subsetName ],
-                                               filterItem.getName()
-                                       );
-
-                                       // Translate subset param name to add the group name, so we
-                                       // get consistent naming. We know that subsets are only within
-                                       // the same group
-                                       subsetNames.push( subsetName );
-                               } );
-
-                               // Set translated subset
-                               filterItem.setSubset( subsetNames );
-                       }
-
-                       items.push( filterItem );
-
-                       // Store default parameter state; in this case, default is defined per filter
-                       if (
-                               model.getType() === 'send_unselected_if_any' ||
-                               model.getType() === 'boolean'
-                       ) {
-                               // Store the default parameter state
-                               // For this group type, parameter values are direct
-                               // We need to convert from a boolean to a string ('1' and '0')
-                               model.defaultParams[ filter.name ] = String( Number( filter.default || 0 ) );
-                       } else if ( model.getType() === 'any_value' ) {
-                               model.defaultParams[ filter.name ] = filter.default;
-                       }
-               } );
-
-               // Add items
-               this.addItems( items );
-
-               // Now that we have all items, we can apply the superset map
-               this.getItems().forEach( function ( filterItem ) {
-                       filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
-               } );
-
-               // Store default parameter state; in this case, default is defined per the
-               // entire group, given by groupDefault method parameter
-               if ( this.getType() === 'string_options' ) {
-                       // Store the default parameter group state
-                       // For this group, the parameter is group name and value is the names
-                       // of selected items
-                       this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
-                               // Current values
-                               groupDefault ?
-                                       groupDefault.split( this.getSeparator() ) :
-                                       [],
-                               // Legal values
-                               this.getItems().map( function ( item ) {
-                                       return item.getParamName();
-                               } )
-                       ).join( this.getSeparator() );
-               } else if ( this.getType() === 'single_option' ) {
-                       defaultParam = groupDefault !== undefined ?
-                               groupDefault : this.getItems()[ 0 ].getParamName();
-
-                       // For this group, the parameter is the group name,
-                       // and a single item can be selected: default or first item
-                       this.defaultParams[ this.getName() ] = defaultParam;
-               }
-
-               // add highlights to defaultParams
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isHighlighted() ) {
-                               this.defaultParams[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
-                       }
-               }.bind( this ) );
-
-               // Store default filter state based on default params
-               this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
-
-               // Check for filters that should be initially selected by their default value
-               if ( this.isSticky() ) {
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( this.defaultFilters, function ( filterName, filterValue ) {
-                               model.getItemByName( filterName ).toggleSelected( filterValue );
-                       } );
-               }
-
-               // Verify that single_option group has at least one item selected
-               if (
-                       this.getType() === 'single_option' &&
-                       this.findSelectedItems().length === 0
-               ) {
-                       defaultParam = groupDefault !== undefined ?
-                               groupDefault : this.getItems()[ 0 ].getParamName();
-
-                       // Single option means there must be a single option
-                       // selected, so we have to either select the default
-                       // or select the first option
-                       this.selectItemByParamName( defaultParam );
-               }
-       };
-
-       /**
-        * Respond to filterItem update event
-        *
-        * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
-        * @fires update
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
-               // Update state
-               var changed = false,
-                       active = this.areAnySelected(),
-                       model = this;
-
-               if ( this.getType() === 'single_option' ) {
-                       // This group must have one item selected always
-                       // and must never have more than one item selected at a time
-                       if ( this.findSelectedItems().length === 0 ) {
-                               // Nothing is selected anymore
-                               // Select the default or the first item
-                               this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
-                                       this.getItems()[ 0 ];
-                               this.currSelected.toggleSelected( true );
-                               changed = true;
-                       } else if ( this.findSelectedItems().length > 1 ) {
-                               // There is more than one item selected
-                               // This should only happen if the item given
-                               // is the one that is selected, so unselect
-                               // all items that is not it
-                               this.findSelectedItems().forEach( function ( itemModel ) {
-                                       // Note that in case the given item is actually
-                                       // not selected, this loop will end up unselecting
-                                       // all items, which would trigger the case above
-                                       // when the last item is unselected anyways
-                                       var selected = itemModel.getName() === item.getName() &&
-                                               item.isSelected();
-
-                                       itemModel.toggleSelected( selected );
-                                       if ( selected ) {
-                                               model.currSelected = itemModel;
-                                       }
-                               } );
-                               changed = true;
-                       }
-               }
-
-               if ( this.isSticky() ) {
-                       // If this group is sticky, then change the default according to the
-                       // current selection.
-                       this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
-               }
-
-               if (
-                       changed ||
-                       this.active !== active ||
-                       this.currSelected !== item
-               ) {
-                       this.active = active;
-                       this.currSelected = item;
-
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Get group active state
-        *
-        * @return {boolean} Active state
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
-               return this.active;
-       };
-
-       /**
-        * Get group hidden state
-        *
-        * @return {boolean} Hidden state
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isHidden = function () {
-               return this.hidden;
-       };
-
-       /**
-        * Get group allow arbitrary state
-        *
-        * @return {boolean} Group allows an arbitrary value from the URL
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isAllowArbitrary = function () {
-               return this.allowArbitrary;
-       };
-
-       /**
-        * Get group maximum value for numeric groups
-        *
-        * @return {number|null} Group max value
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getMaxValue = function () {
-               return this.numericRange && this.numericRange.max !== undefined ?
-                       this.numericRange.max : null;
-       };
-
-       /**
-        * Get group minimum value for numeric groups
-        *
-        * @return {number|null} Group max value
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getMinValue = function () {
-               return this.numericRange && this.numericRange.min !== undefined ?
-                       this.numericRange.min : null;
-       };
-
-       /**
-        * Get group name
-        *
-        * @return {string} Group name
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getName = function () {
-               return this.name;
-       };
-
-       /**
-        * Get the default param state of this group
-        *
-        * @return {Object} Default param state
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () {
-               return this.defaultParams;
-       };
-
-       /**
-        * Get the default filter state of this group
-        *
-        * @return {Object} Default filter state
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getDefaultFilters = function () {
-               return this.defaultFilters;
-       };
-
-       /**
-        * This is for a single_option and string_options group types
-        * it returns the value of the default
-        *
-        * @return {string} Value of the default
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getDefaulParamValue = function () {
-               return this.defaultParams[ this.getName() ];
-       };
-       /**
-        * Get the messags defining the 'whats this' popup for this group
-        *
-        * @return {Object} What's this messages
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
-               return this.whatsThis;
-       };
-
-       /**
-        * Check whether this group has a 'what's this' message
-        *
-        * @return {boolean} This group has a what's this message
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
-               return !!this.whatsThis.body;
-       };
-
-       /**
-        * Get the conflicts associated with the entire group.
-        * Conflict object is set up by filter name keys and conflict
-        * definition. For example:
-        * [
-        *     {
-        *         filterName: {
-        *             filter: filterName,
-        *             group: group1
-        *         }
-        *     },
-        *     {
-        *         filterName2: {
-        *             filter: filterName2,
-        *             group: group2
-        *         }
-        *     }
-        * ]
-        * @return {Object} Conflict definition
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
-               return this.conflicts;
-       };
-
-       /**
-        * Set conflicts for this group. See #getConflicts for the expected
-        * structure of the definition.
-        *
-        * @param {Object} conflicts Conflicts for this group
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
-               this.conflicts = conflicts;
-       };
-
-       /**
-        * Set conflicts for each filter item in the group based on the
-        * given conflict map
-        *
-        * @param {Object} conflicts Object representing the conflict map,
-        *  keyed by the item name, where its value is an object for all its conflicts
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( conflicts[ filterItem.getName() ] ) {
-                               filterItem.setConflicts( conflicts[ filterItem.getName() ] );
-                       }
-               } );
-       };
-
-       /**
-        * Check whether this item has a potential conflict with the given item
-        *
-        * This checks whether the given item is in the list of conflicts of
-        * the current item, but makes no judgment about whether the conflict
-        * is currently at play (either one of the items may not be selected)
-        *
-        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
-        * @return {boolean} This item has a conflict with the given item
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
-               return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
-       };
-
-       /**
-        * Check whether there are any items selected
-        *
-        * @return {boolean} Any items in the group are selected
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () {
-               return this.getItems().some( function ( filterItem ) {
-                       return filterItem.isSelected();
-               } );
-       };
-
-       /**
-        * Check whether all items selected
-        *
-        * @return {boolean} All items are selected
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () {
-               var selected = [],
-                       unselected = [];
-
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isSelected() ) {
-                               selected.push( filterItem );
-                       } else {
-                               unselected.push( filterItem );
-                       }
-               } );
-
-               if ( unselected.length === 0 ) {
-                       return true;
-               }
-
-               // check if every unselected is a subset of a selected
-               return unselected.every( function ( unselectedFilterItem ) {
-                       return selected.some( function ( selectedFilterItem ) {
-                               return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
-                       } );
-               } );
-       };
-
-       /**
-        * Get all selected items in this group
-        *
-        * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
-        * @return {mw.rcfilters.dm.FilterItem[]} Selected items
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.findSelectedItems = function ( excludeItem ) {
-               var excludeName = ( excludeItem && excludeItem.getName() ) || '';
-
-               return this.getItems().filter( function ( item ) {
-                       return item.getName() !== excludeName && item.isSelected();
-               } );
-       };
-
-       /**
-        * Check whether all selected items are in conflict with the given item
-        *
-        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
-        * @return {boolean} All selected items are in conflict with this item
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
-               var selectedItems = this.findSelectedItems( filterItem );
-
-               return selectedItems.length > 0 &&
-                       (
-                               // The group as a whole is in conflict with this item
-                               this.existsInConflicts( filterItem ) ||
-                               // All selected items are in conflict individually
-                               selectedItems.every( function ( selectedFilter ) {
-                                       return selectedFilter.existsInConflicts( filterItem );
-                               } )
-                       );
-       };
-
-       /**
-        * Check whether any of the selected items are in conflict with the given item
-        *
-        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
-        * @return {boolean} Any of the selected items are in conflict with this item
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
-               var selectedItems = this.findSelectedItems( filterItem );
-
-               return selectedItems.length > 0 && (
-                       // The group as a whole is in conflict with this item
-                       this.existsInConflicts( filterItem ) ||
-                       // Any selected items are in conflict individually
-                       selectedItems.some( function ( selectedFilter ) {
-                               return selectedFilter.existsInConflicts( filterItem );
-                       } )
-               );
-       };
-
-       /**
-        * Get the parameter representation from this group
-        *
-        * @param {Object} [filterRepresentation] An object defining the state
-        *  of the filters in this group, keyed by their name and current selected
-        *  state value.
-        * @return {Object} Parameter representation
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
-               var values,
-                       areAnySelected = false,
-                       buildFromCurrentState = !filterRepresentation,
-                       defaultFilters = this.getDefaultFilters(),
-                       result = {},
-                       model = this,
-                       filterParamNames = {},
-                       getSelectedParameter = function ( filters ) {
-                               var item,
-                                       selected = [];
-
-                               // Find if any are selected
-                               // eslint-disable-next-line jquery/no-each-util
-                               $.each( filters, function ( name, value ) {
-                                       if ( value ) {
-                                               selected.push( name );
-                                       }
-                               } );
-
-                               item = model.getItemByName( selected[ 0 ] );
-                               return ( item && item.getParamName() ) || '';
-                       };
-
-               filterRepresentation = filterRepresentation || {};
-
-               // Create or complete the filterRepresentation definition
-               this.getItems().forEach( function ( item ) {
-                       // Map filter names to their parameter names
-                       filterParamNames[ item.getName() ] = item.getParamName();
-
-                       if ( buildFromCurrentState ) {
-                               // This means we have not been given a filter representation
-                               // so we are building one based on current state
-                               filterRepresentation[ item.getName() ] = item.getValue();
-                       } else if ( filterRepresentation[ item.getName() ] === undefined ) {
-                               // We are given a filter representation, but we have to make
-                               // sure that we fill in the missing filters if there are any
-                               // we will assume they are all falsey
-                               if ( model.isSticky() ) {
-                                       filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
-                               } else {
-                                       filterRepresentation[ item.getName() ] = false;
-                               }
-                       }
-
-                       if ( filterRepresentation[ item.getName() ] ) {
-                               areAnySelected = true;
-                       }
-               } );
-
-               // Build result
-               if (
-                       this.getType() === 'send_unselected_if_any' ||
-                       this.getType() === 'boolean' ||
-                       this.getType() === 'any_value'
-               ) {
-                       // First, check if any of the items are selected at all.
-                       // If none is selected, we're treating it as if they are
-                       // all false
-
-                       // Go over the items and define the correct values
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( filterRepresentation, function ( name, value ) {
-                               // We must store all parameter values as strings '0' or '1'
-                               if ( model.getType() === 'send_unselected_if_any' ) {
-                                       result[ filterParamNames[ name ] ] = areAnySelected ?
-                                               String( Number( !value ) ) :
-                                               '0';
-                               } else if ( model.getType() === 'boolean' ) {
-                                       // Representation is straight-forward and direct from
-                                       // the parameter value to the filter state
-                                       result[ filterParamNames[ name ] ] = String( Number( !!value ) );
-                               } else if ( model.getType() === 'any_value' ) {
-                                       result[ filterParamNames[ name ] ] = value;
-                               }
-                       } );
-               } else if ( this.getType() === 'string_options' ) {
-                       values = [];
-
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( filterRepresentation, function ( name, value ) {
-                               // Collect values
-                               if ( value ) {
-                                       values.push( filterParamNames[ name ] );
-                               }
-                       } );
-
-                       result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
-                               'all' : values.join( this.getSeparator() );
-               } else if ( this.getType() === 'single_option' ) {
-                       result[ this.getName() ] = getSelectedParameter( filterRepresentation );
-               }
-
-               return result;
-       };
-
-       /**
-        * Get the filter representation this group would provide
-        * based on given parameter states.
-        *
-        * @param {Object} [paramRepresentation] An object defining a parameter
-        *  state to translate the filter state from. If not given, an object
-        *  representing all filters as falsey is returned; same as if the parameter
-        *  given were an empty object, or had some of the filters missing.
-        * @return {Object} Filter representation
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
-               var areAnySelected, paramValues, item, currentValue,
-                       oneWasSelected = false,
-                       defaultParams = this.getDefaultParams(),
-                       expandedParams = $.extend( true, {}, paramRepresentation ),
-                       model = this,
-                       paramToFilterMap = {},
-                       result = {};
-
-               if ( this.isSticky() ) {
-                       // If the group is sticky, check if all parameters are represented
-                       // and for those that aren't represented, add them with their default
-                       // values
-                       paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
-               }
-
-               paramRepresentation = paramRepresentation || {};
-               if (
-                       this.getType() === 'send_unselected_if_any' ||
-                       this.getType() === 'boolean' ||
-                       this.getType() === 'any_value'
-               ) {
-                       // Go over param representation; map and check for selections
-                       this.getItems().forEach( function ( filterItem ) {
-                               var paramName = filterItem.getParamName();
-
-                               expandedParams[ paramName ] = paramRepresentation[ paramName ] || '0';
-                               paramToFilterMap[ paramName ] = filterItem;
-
-                               if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
-                                       areAnySelected = true;
-                               }
-                       } );
-
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( expandedParams, function ( paramName, paramValue ) {
-                               var filterItem = paramToFilterMap[ paramName ];
-
-                               if ( model.getType() === 'send_unselected_if_any' ) {
-                                       // Flip the definition between the parameter
-                                       // state and the filter state
-                                       // This is what the 'toggleSelected' value of the filter is
-                                       result[ filterItem.getName() ] = areAnySelected ?
-                                               !Number( paramValue ) :
-                                               // Otherwise, there are no selected items in the
-                                               // group, which means the state is false
-                                               false;
-                               } else if ( model.getType() === 'boolean' ) {
-                                       // Straight-forward definition of state
-                                       result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
-                               } else if ( model.getType() === 'any_value' ) {
-                                       result[ filterItem.getName() ] = paramRepresentation[ filterItem.getParamName() ];
-                               }
-                       } );
-               } else if ( this.getType() === 'string_options' ) {
-                       currentValue = paramRepresentation[ this.getName() ] || '';
-
-                       // Normalize the given parameter values
-                       paramValues = mw.rcfilters.utils.normalizeParamOptions(
-                               // Given
-                               currentValue.split(
-                                       this.getSeparator()
-                               ),
-                               // Allowed values
-                               this.getItems().map( function ( filterItem ) {
-                                       return filterItem.getParamName();
-                               } )
-                       );
-                       // Translate the parameter values into a filter selection state
-                       this.getItems().forEach( function ( filterItem ) {
-                               // All true (either because all values are written or the term 'all' is written)
-                               // is the same as all filters set to true
-                               result[ filterItem.getName() ] = (
-                                       // If it is the word 'all'
-                                       paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
-                                       // All values are written
-                                       paramValues.length === model.getItemCount()
-                               ) ?
-                                       true :
-                                       // Otherwise, the filter is selected only if it appears in the parameter values
-                                       paramValues.indexOf( filterItem.getParamName() ) > -1;
-                       } );
-               } else if ( this.getType() === 'single_option' ) {
-                       // There is parameter that fits a single filter and if not, get the default
-                       this.getItems().forEach( function ( filterItem ) {
-                               var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
-
-                               result[ filterItem.getName() ] = selected;
-                               oneWasSelected = oneWasSelected || selected;
-                       } );
-               }
-
-               // Go over result and make sure all filters are represented.
-               // If any filters are missing, they will get a falsey value
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( result[ filterItem.getName() ] === undefined ) {
-                               result[ filterItem.getName() ] = this.getFalsyValue();
-                       }
-               }.bind( this ) );
-
-               // Make sure that at least one option is selected in
-               // single_option groups, no matter what path was taken
-               // If none was selected by the given definition, then
-               // we need to select the one in the base state -- either
-               // the default given, or the first item
-               if (
-                       this.getType() === 'single_option' &&
-                       !oneWasSelected
-               ) {
-                       item = this.getItems()[ 0 ];
-                       if ( defaultParams[ this.getName() ] ) {
-                               item = this.getItemByParamName( defaultParams[ this.getName() ] );
-                       }
-
-                       result[ item.getName() ] = true;
-               }
-
-               return result;
-       };
-
-       /**
-        * @return {*} The appropriate falsy value for this group type
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getFalsyValue = function () {
-               return this.getType() === 'any_value' ? '' : false;
-       };
-
-       /**
-        * Get current selected state of all filter items in this group
-        *
-        * @return {Object} Selected state
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getSelectedState = function () {
-               var state = {};
-
-               this.getItems().forEach( function ( filterItem ) {
-                       state[ filterItem.getName() ] = filterItem.getValue();
-               } );
-
-               return state;
-       };
-
-       /**
-        * Get item by its filter name
-        *
-        * @param {string} filterName Filter name
-        * @return {mw.rcfilters.dm.FilterItem} Filter item
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getItemByName = function ( filterName ) {
-               return this.getItems().filter( function ( item ) {
-                       return item.getName() === filterName;
-               } )[ 0 ];
-       };
-
-       /**
-        * Select an item by its parameter name
-        *
-        * @param {string} paramName Filter parameter name
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
-               this.getItems().forEach( function ( item ) {
-                       item.toggleSelected( item.getParamName() === String( paramName ) );
-               } );
-       };
-
-       /**
-        * Get item by its parameter name
-        *
-        * @param {string} paramName Parameter name
-        * @return {mw.rcfilters.dm.FilterItem} Filter item
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
-               return this.getItems().filter( function ( item ) {
-                       return item.getParamName() === String( paramName );
-               } )[ 0 ];
-       };
-
-       /**
-        * Get group type
-        *
-        * @return {string} Group type
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
-               return this.type;
-       };
-
-       /**
-        * Check whether this group is represented by a single parameter
-        * or whether each item is its own parameter
-        *
-        * @return {boolean} This group is a single parameter
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isPerGroupRequestParameter = function () {
-               return (
-                       this.getType() === 'string_options' ||
-                       this.getType() === 'single_option'
-               );
-       };
-
-       /**
-        * Get display group
-        *
-        * @return {string} Display group
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getView = function () {
-               return this.view;
-       };
-
-       /**
-        * Get the prefix used for the filter names inside this group.
-        *
-        * @param {string} [name] Filter name to prefix
-        * @return {string} Group prefix
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
-               return this.getName() + '__';
-       };
-
-       /**
-        * Get a filter name with the prefix used for the filter names inside this group.
-        *
-        * @param {string} name Filter name to prefix
-        * @return {string} Group prefix
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) {
-               return this.getNamePrefix() + name;
-       };
-
-       /**
-        * Get group's title
-        *
-        * @return {string} Title
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
-               return this.title;
-       };
-
-       /**
-        * Get group's values separator
-        *
-        * @return {string} Values separator
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
-               return this.separator;
-       };
-
-       /**
-        * Check whether the group is defined as full coverage
-        *
-        * @return {boolean} Group is full coverage
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
-               return this.fullCoverage;
-       };
-
-       /**
-        * Check whether the group is defined as sticky default
-        *
-        * @return {boolean} Group is sticky default
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isSticky = function () {
-               return this.sticky;
-       };
-
-       /**
-        * Normalize a value given to this group. This is mostly for correcting
-        * arbitrary values for 'single option' groups, given by the user settings
-        * or the URL that can go outside the limits that are allowed.
-        *
-        * @param  {string} value Given value
-        * @return {string} Corrected value
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.normalizeArbitraryValue = function ( value ) {
-               if (
-                       this.getType() === 'single_option' &&
-                       this.isAllowArbitrary()
-               ) {
-                       if (
-                               this.getMaxValue() !== null &&
-                               value > this.getMaxValue()
-                       ) {
-                               // Change the value to the actual max value
-                               return String( this.getMaxValue() );
-                       } else if (
-                               this.getMinValue() !== null &&
-                               value < this.getMinValue()
-                       ) {
-                               // Change the value to the actual min value
-                               return String( this.getMinValue() );
-                       }
-               }
-
-               return value;
-       };
-
-       /**
-        * Toggle the visibility of this group
-        *
-        * @param {boolean} [isVisible] Item is visible
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.toggleVisible = function ( isVisible ) {
-               isVisible = isVisible === undefined ? !this.visible : isVisible;
-
-               if ( this.visible !== isVisible ) {
-                       this.visible = isVisible;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Check whether the group is visible
-        *
-        * @return {boolean} Group is visible
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.isVisible = function () {
-               return this.visible;
-       };
-
-       /**
-        * Set the visibility of the items under this group by the given items array
-        *
-        * @param {mw.rcfilters.dm.ItemModel[]} visibleItems An array of visible items
-        */
-       mw.rcfilters.dm.FilterGroup.prototype.setVisibleItems = function ( visibleItems ) {
-               this.getItems().forEach( function ( itemModel ) {
-                       itemModel.toggleVisible( visibleItems.indexOf( itemModel ) !== -1 );
-               } );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js
deleted file mode 100644 (file)
index dac61b2..0000000
+++ /dev/null
@@ -1,400 +0,0 @@
-( function () {
-       /**
-        * Filter item model
-        *
-        * @extends mw.rcfilters.dm.ItemModel
-        *
-        * @constructor
-        * @param {string} param Filter param name
-        * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
-        * @param {Object} config Configuration object
-        * @cfg {string[]} [excludes=[]] A list of filter names this filter, if
-        *  selected, makes inactive.
-        * @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
-        * @cfg {Object} [conflicts] Defines the conflicts for this filter
-        * @cfg {boolean} [visible=true] The visibility of the group
-        */
-       mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
-               config = config || {};
-
-               this.groupModel = groupModel;
-
-               // Parent
-               mw.rcfilters.dm.FilterItem.parent.call( this, param, $.extend( {
-                       namePrefix: this.groupModel.getNamePrefix()
-               }, config ) );
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-
-               // Interaction definitions
-               this.subset = config.subset || [];
-               this.conflicts = config.conflicts || {};
-               this.superset = [];
-               this.visible = config.visible === undefined ? true : !!config.visible;
-
-               // Interaction states
-               this.included = false;
-               this.conflicted = false;
-               this.fullyCovered = false;
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.dm.FilterItem, mw.rcfilters.dm.ItemModel );
-
-       /* Methods */
-
-       /**
-        * Return the representation of the state of this item.
-        *
-        * @return {Object} State of the object
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getState = function () {
-               return {
-                       selected: this.isSelected(),
-                       included: this.isIncluded(),
-                       conflicted: this.isConflicted(),
-                       fullyCovered: this.isFullyCovered()
-               };
-       };
-
-       /**
-        * Get the message for the display area for the currently active conflict
-        *
-        * @private
-        * @return {string} Conflict result message key
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getCurrentConflictResultMessage = function () {
-               var details = {};
-
-               // First look in filter's own conflicts
-               details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
-               if ( !details.message ) {
-                       // Fall back onto conflicts in the group
-                       details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
-               }
-
-               return details.message;
-       };
-
-       /**
-        * Get the details of the active conflict on this filter
-        *
-        * @private
-        * @param {Object} conflicts Conflicts to examine
-        * @param {string} [key='contextDescription'] Message key
-        * @return {Object} Object with conflict message and conflict items
-        * @return {string} return.message Conflict message
-        * @return {string[]} return.names Conflicting item labels
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
-               var group,
-                       conflictMessage = '',
-                       itemLabels = [];
-
-               key = key || 'contextDescription';
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( conflicts, function ( filterName, conflict ) {
-                       if ( !conflict.item.isSelected() ) {
-                               return;
-                       }
-
-                       if ( !conflictMessage ) {
-                               conflictMessage = conflict[ key ];
-                               group = conflict.group;
-                       }
-
-                       if ( group === conflict.group ) {
-                               itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) );
-                       }
-               } );
-
-               return {
-                       message: conflictMessage,
-                       names: itemLabels
-               };
-
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getStateMessage = function () {
-               var messageKey, details, superset,
-                       affectingItems = [];
-
-               if ( this.isSelected() ) {
-                       if ( this.isConflicted() ) {
-                               // First look in filter's own conflicts
-                               details = this.getConflictDetails( this.getOwnConflicts() );
-                               if ( !details.message ) {
-                                       // Fall back onto conflicts in the group
-                                       details = this.getConflictDetails( this.getGroupModel().getConflicts() );
-                               }
-
-                               messageKey = details.message;
-                               affectingItems = details.names;
-                       } else if ( this.isIncluded() && !this.isHighlighted() ) {
-                               // We only show the 'no effect' full-coverage message
-                               // if the item is also not highlighted. See T161273
-                               superset = this.getSuperset();
-                               // For this message we need to collect the affecting superset
-                               affectingItems = this.getGroupModel().findSelectedItems( this )
-                                       .filter( function ( item ) {
-                                               return superset.indexOf( item.getName() ) !== -1;
-                                       } )
-                                       .map( function ( item ) {
-                                               return mw.msg( 'quotation-marks', item.getLabel() );
-                                       } );
-
-                               messageKey = 'rcfilters-state-message-subset';
-                       } else if ( this.isFullyCovered() && !this.isHighlighted() ) {
-                               affectingItems = this.getGroupModel().findSelectedItems( this )
-                                       .map( function ( item ) {
-                                               return mw.msg( 'quotation-marks', item.getLabel() );
-                                       } );
-
-                               messageKey = 'rcfilters-state-message-fullcoverage';
-                       }
-               }
-
-               if ( messageKey ) {
-                       // Build message
-                       return mw.msg(
-                               messageKey,
-                               mw.language.listToText( affectingItems ),
-                               affectingItems.length
-                       );
-               }
-
-               // Display description
-               return this.getDescription();
-       };
-
-       /**
-        * Get the model of the group this filter belongs to
-        *
-        * @return {mw.rcfilters.dm.FilterGroup} Filter group model
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getGroupModel = function () {
-               return this.groupModel;
-       };
-
-       /**
-        * Get the group name this filter belongs to
-        *
-        * @return {string} Filter group name
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getGroupName = function () {
-               return this.groupModel.getName();
-       };
-
-       /**
-        * Get filter subset
-        * This is a list of filter names that are defined to be included
-        * when this filter is selected.
-        *
-        * @return {string[]} Filter subset
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getSubset = function () {
-               return this.subset;
-       };
-
-       /**
-        * Get filter superset
-        * This is a generated list of filters that define this filter
-        * to be included when either of them is selected.
-        *
-        * @return {string[]} Filter superset
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getSuperset = function () {
-               return this.superset;
-       };
-
-       /**
-        * Check whether the filter is currently in a conflict state
-        *
-        * @return {boolean} Filter is in conflict state
-        */
-       mw.rcfilters.dm.FilterItem.prototype.isConflicted = function () {
-               return this.conflicted;
-       };
-
-       /**
-        * Check whether the filter is currently in an already included subset
-        *
-        * @return {boolean} Filter is in an already-included subset
-        */
-       mw.rcfilters.dm.FilterItem.prototype.isIncluded = function () {
-               return this.included;
-       };
-
-       /**
-        * Check whether the filter is currently fully covered
-        *
-        * @return {boolean} Filter is in fully-covered state
-        */
-       mw.rcfilters.dm.FilterItem.prototype.isFullyCovered = function () {
-               return this.fullyCovered;
-       };
-
-       /**
-        * Get all conflicts associated with this filter or its group
-        *
-        * Conflict object is set up by filter name keys and conflict
-        * definition. For example:
-        *
-        *  {
-        *      filterName: {
-        *          filter: filterName,
-        *          group: group1,
-        *          label: itemLabel,
-        *          item: itemModel
-        *      }
-        *      filterName2: {
-        *          filter: filterName2,
-        *          group: group2
-        *          label: itemLabel2,
-        *          item: itemModel2
-        *      }
-        *  }
-        *
-        * @return {Object} Filter conflicts
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getConflicts = function () {
-               return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
-       };
-
-       /**
-        * Get the conflicts associated with this filter
-        *
-        * @return {Object} Filter conflicts
-        */
-       mw.rcfilters.dm.FilterItem.prototype.getOwnConflicts = function () {
-               return this.conflicts;
-       };
-
-       /**
-        * Set conflicts for this filter. See #getConflicts for the expected
-        * structure of the definition.
-        *
-        * @param {Object} conflicts Conflicts for this filter
-        */
-       mw.rcfilters.dm.FilterItem.prototype.setConflicts = function ( conflicts ) {
-               this.conflicts = conflicts || {};
-       };
-
-       /**
-        * Set filter superset
-        *
-        * @param {string[]} superset Filter superset
-        */
-       mw.rcfilters.dm.FilterItem.prototype.setSuperset = function ( superset ) {
-               this.superset = superset || [];
-       };
-
-       /**
-        * Set filter subset
-        *
-        * @param {string[]} subset Filter subset
-        */
-       mw.rcfilters.dm.FilterItem.prototype.setSubset = function ( subset ) {
-               this.subset = subset || [];
-       };
-
-       /**
-        * Check whether a filter exists in the subset list for this filter
-        *
-        * @param {string} filterName Filter name
-        * @return {boolean} Filter name is in the subset list
-        */
-       mw.rcfilters.dm.FilterItem.prototype.existsInSubset = function ( filterName ) {
-               return this.subset.indexOf( filterName ) > -1;
-       };
-
-       /**
-        * Check whether this item has a potential conflict with the given item
-        *
-        * This checks whether the given item is in the list of conflicts of
-        * the current item, but makes no judgment about whether the conflict
-        * is currently at play (either one of the items may not be selected)
-        *
-        * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
-        * @return {boolean} This item has a conflict with the given item
-        */
-       mw.rcfilters.dm.FilterItem.prototype.existsInConflicts = function ( filterItem ) {
-               return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
-       };
-
-       /**
-        * Set the state of this filter as being conflicted
-        * (This means any filters in its conflicts are selected)
-        *
-        * @param {boolean} [conflicted] Filter is in conflict state
-        * @fires update
-        */
-       mw.rcfilters.dm.FilterItem.prototype.toggleConflicted = function ( conflicted ) {
-               conflicted = conflicted === undefined ? !this.conflicted : conflicted;
-
-               if ( this.conflicted !== conflicted ) {
-                       this.conflicted = conflicted;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Set the state of this filter as being already included
-        * (This means any filters in its superset are selected)
-        *
-        * @param {boolean} [included] Filter is included as part of a subset
-        * @fires update
-        */
-       mw.rcfilters.dm.FilterItem.prototype.toggleIncluded = function ( included ) {
-               included = included === undefined ? !this.included : included;
-
-               if ( this.included !== included ) {
-                       this.included = included;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Toggle the fully covered state of the item
-        *
-        * @param {boolean} [isFullyCovered] Filter is fully covered
-        * @fires update
-        */
-       mw.rcfilters.dm.FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
-               isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;
-
-               if ( this.fullyCovered !== isFullyCovered ) {
-                       this.fullyCovered = isFullyCovered;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Toggle the visibility of this item
-        *
-        * @param {boolean} [isVisible] Item is visible
-        */
-       mw.rcfilters.dm.FilterItem.prototype.toggleVisible = function ( isVisible ) {
-               isVisible = isVisible === undefined ? !this.visible : !!isVisible;
-
-               if ( this.visible !== isVisible ) {
-                       this.visible = isVisible;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Check whether the item is visible
-        *
-        * @return {boolean} Item is visible
-        */
-       mw.rcfilters.dm.FilterItem.prototype.isVisible = function () {
-               return this.visible;
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
deleted file mode 100644 (file)
index 5d51d10..0000000
+++ /dev/null
@@ -1,1295 +0,0 @@
-( function () {
-       /**
-        * View model for the filters selection and display
-        *
-        * @mixins OO.EventEmitter
-        * @mixins OO.EmitterList
-        *
-        * @constructor
-        */
-       mw.rcfilters.dm.FiltersViewModel = function MwRcfiltersDmFiltersViewModel() {
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-               OO.EmitterList.call( this );
-
-               this.groups = {};
-               this.defaultParams = {};
-               this.highlightEnabled = false;
-               this.parameterMap = {};
-               this.emptyParameterState = null;
-
-               this.views = {};
-               this.currentView = 'default';
-               this.searchQuery = null;
-
-               // Events
-               this.aggregate( { update: 'filterItemUpdate' } );
-               this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
-       };
-
-       /* Initialization */
-       OO.initClass( mw.rcfilters.dm.FiltersViewModel );
-       OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EventEmitter );
-       OO.mixinClass( mw.rcfilters.dm.FiltersViewModel, OO.EmitterList );
-
-       /* Events */
-
-       /**
-        * @event initialize
-        *
-        * Filter list is initialized
-        */
-
-       /**
-        * @event update
-        *
-        * Model has been updated
-        */
-
-       /**
-        * @event itemUpdate
-        * @param {mw.rcfilters.dm.FilterItem} item Filter item updated
-        *
-        * Filter item has changed
-        */
-
-       /**
-        * @event highlightChange
-        * @param {boolean} Highlight feature is enabled
-        *
-        * Highlight feature has been toggled enabled or disabled
-        */
-
-       /* Methods */
-
-       /**
-        * Re-assess the states of filter items based on the interactions between them
-        *
-        * @param {mw.rcfilters.dm.FilterItem} [item] Changed item. If not given, the
-        *  method will go over the state of all items
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.reassessFilterInteractions = function ( item ) {
-               var allSelected,
-                       model = this,
-                       iterationItems = item !== undefined ? [ item ] : this.getItems();
-
-               iterationItems.forEach( function ( checkedItem ) {
-                       var allCheckedItems = checkedItem.getSubset().concat( [ checkedItem.getName() ] ),
-                               groupModel = checkedItem.getGroupModel();
-
-                       // Check for subsets (included filters) plus the item itself:
-                       allCheckedItems.forEach( function ( filterItemName ) {
-                               var itemInSubset = model.getItemByName( filterItemName );
-
-                               itemInSubset.toggleIncluded(
-                                       // If any of itemInSubset's supersets are selected, this item
-                                       // is included
-                                       itemInSubset.getSuperset().some( function ( supersetName ) {
-                                               return ( model.getItemByName( supersetName ).isSelected() );
-                                       } )
-                               );
-                       } );
-
-                       // Update coverage for the changed group
-                       if ( groupModel.isFullCoverage() ) {
-                               allSelected = groupModel.areAllSelected();
-                               groupModel.getItems().forEach( function ( filterItem ) {
-                                       filterItem.toggleFullyCovered( allSelected );
-                               } );
-                       }
-               } );
-
-               // Check for conflicts
-               // In this case, we must go over all items, since
-               // conflicts are bidirectional and depend not only on
-               // individual items, but also on the selected states of
-               // the groups they're in.
-               this.getItems().forEach( function ( filterItem ) {
-                       var inConflict = false,
-                               filterItemGroup = filterItem.getGroupModel();
-
-                       // For each item, see if that item is still conflicting
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( model.groups, function ( groupName, groupModel ) {
-                               if ( filterItem.getGroupName() === groupName ) {
-                                       // Check inside the group
-                                       inConflict = groupModel.areAnySelectedInConflictWith( filterItem );
-                               } else {
-                                       // According to the spec, if two items conflict from two different
-                                       // groups, the conflict only lasts if the groups **only have selected
-                                       // items that are conflicting**. If a group has selected items that
-                                       // are conflicting and non-conflicting, the scope of the result has
-                                       // expanded enough to completely remove the conflict.
-
-                                       // For example, see two groups with conflicts:
-                                       // userExpLevel: [
-                                       //   {
-                                       //     name: 'experienced',
-                                       //     conflicts: [ 'unregistered' ]
-                                       //   }
-                                       // ],
-                                       // registration: [
-                                       //   {
-                                       //     name: 'registered',
-                                       //   },
-                                       //   {
-                                       //     name: 'unregistered',
-                                       //   }
-                                       // ]
-                                       // If we select 'experienced', then 'unregistered' is in conflict (and vice versa),
-                                       // because, inherently, 'experienced' filter only includes registered users, and so
-                                       // both filters are in conflict with one another.
-                                       // However, the minute we select 'registered', the scope of our results
-                                       // has expanded to no longer have a conflict with 'experienced' filter, and
-                                       // so the conflict is removed.
-
-                                       // In our case, we need to check if the entire group conflicts with
-                                       // the entire item's group, so we follow the above spec
-                                       inConflict = (
-                                               // The foreign group is in conflict with this item
-                                               groupModel.areAllSelectedInConflictWith( filterItem ) &&
-                                               // Every selected member of the item's own group is also
-                                               // in conflict with the other group
-                                               filterItemGroup.findSelectedItems().every( function ( otherGroupItem ) {
-                                                       return groupModel.areAllSelectedInConflictWith( otherGroupItem );
-                                               } )
-                                       );
-                               }
-
-                               // If we're in conflict, this will return 'false' which
-                               // will break the loop. Otherwise, we're not in conflict
-                               // and the loop continues
-                               return !inConflict;
-                       } );
-
-                       // Toggle the item state
-                       filterItem.toggleConflicted( inConflict );
-               } );
-       };
-
-       /**
-        * Get whether the model has any conflict in its items
-        *
-        * @return {boolean} There is a conflict
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.hasConflict = function () {
-               return this.getItems().some( function ( filterItem ) {
-                       return filterItem.isSelected() && filterItem.isConflicted();
-               } );
-       };
-
-       /**
-        * Get the first item with a current conflict
-        *
-        * @return {mw.rcfilters.dm.FilterItem} Conflicted item
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFirstConflictedItem = function () {
-               var conflictedItem;
-
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isSelected() && filterItem.isConflicted() ) {
-                               conflictedItem = filterItem;
-                               return false;
-                       }
-               } );
-
-               return conflictedItem;
-       };
-
-       /**
-        * Set filters and preserve a group relationship based on
-        * the definition given by an object
-        *
-        * @param {Array} filterGroups Filters definition
-        * @param {Object} [views] Extra views definition
-        *  Expected in the following format:
-        *  {
-        *     namespaces: {
-        *       label: 'namespaces', // Message key
-        *       trigger: ':',
-        *       groups: [
-        *         {
-        *            // Group info
-        *            name: 'namespaces' // Parameter name
-        *            title: 'namespaces' // Message key
-        *            type: 'string_options',
-        *            separator: ';',
-        *            labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
-        *            fullCoverage: true
-        *            items: []
-        *         }
-        *       ]
-        *     }
-        *  }
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filterGroups, views ) {
-               var filterConflictResult, groupConflictResult,
-                       allViews = {},
-                       model = this,
-                       items = [],
-                       groupConflictMap = {},
-                       filterConflictMap = {},
-                       /*!
-                        * Expand a conflict definition from group name to
-                        * the list of all included filters in that group.
-                        * We do this so that the direct relationship in the
-                        * models are consistently item->items rather than
-                        * mixing item->group with item->item.
-                        *
-                        * @param {Object} obj Conflict definition
-                        * @return {Object} Expanded conflict definition
-                        */
-                       expandConflictDefinitions = function ( obj ) {
-                               var result = {};
-
-                               // eslint-disable-next-line jquery/no-each-util
-                               $.each( obj, function ( key, conflicts ) {
-                                       var filterName,
-                                               adjustedConflicts = {};
-
-                                       conflicts.forEach( function ( conflict ) {
-                                               var filter;
-
-                                               if ( conflict.filter ) {
-                                                       filterName = model.groups[ conflict.group ].getPrefixedName( conflict.filter );
-                                                       filter = model.getItemByName( filterName );
-
-                                                       // Rename
-                                                       adjustedConflicts[ filterName ] = $.extend(
-                                                               {},
-                                                               conflict,
-                                                               {
-                                                                       filter: filterName,
-                                                                       item: filter
-                                                               }
-                                                       );
-                                               } else {
-                                                       // This conflict is for an entire group. Split it up to
-                                                       // represent each filter
-
-                                                       // Get the relevant group items
-                                                       model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
-                                                               // Rebuild the conflict
-                                                               adjustedConflicts[ groupItem.getName() ] = $.extend(
-                                                                       {},
-                                                                       conflict,
-                                                                       {
-                                                                               filter: groupItem.getName(),
-                                                                               item: groupItem
-                                                                       }
-                                                               );
-                                                       } );
-                                               }
-                                       } );
-
-                                       result[ key ] = adjustedConflicts;
-                               } );
-
-                               return result;
-                       };
-
-               // Reset
-               this.clearItems();
-               this.groups = {};
-               this.views = {};
-
-               // Clone
-               filterGroups = OO.copy( filterGroups );
-
-               // Normalize definition from the server
-               filterGroups.forEach( function ( data ) {
-                       var i;
-                       // What's this information needs to be normalized
-                       data.whatsThis = {
-                               body: data.whatsThisBody,
-                               header: data.whatsThisHeader,
-                               linkText: data.whatsThisLinkText,
-                               url: data.whatsThisUrl
-                       };
-
-                       // Title is a msg-key
-                       data.title = data.title ? mw.msg( data.title ) : data.name;
-
-                       // Filters are given to us with msg-keys, we need
-                       // to translate those before we hand them off
-                       for ( i = 0; i < data.filters.length; i++ ) {
-                               data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
-                               data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
-                       }
-               } );
-
-               // Collect views
-               allViews = $.extend( true, {
-                       default: {
-                               title: mw.msg( 'rcfilters-filterlist-title' ),
-                               groups: filterGroups
-                       }
-               }, views );
-
-               // Go over all views
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( allViews, function ( viewName, viewData ) {
-                       // Define the view
-                       model.views[ viewName ] = {
-                               name: viewData.name,
-                               title: viewData.title,
-                               trigger: viewData.trigger
-                       };
-
-                       // Go over groups
-                       viewData.groups.forEach( function ( groupData ) {
-                               var group = groupData.name;
-
-                               if ( !model.groups[ group ] ) {
-                                       model.groups[ group ] = new mw.rcfilters.dm.FilterGroup(
-                                               group,
-                                               $.extend( true, {}, groupData, { view: viewName } )
-                                       );
-                               }
-
-                               model.groups[ group ].initializeFilters( groupData.filters, groupData.default );
-                               items = items.concat( model.groups[ group ].getItems() );
-
-                               // Prepare conflicts
-                               if ( groupData.conflicts ) {
-                                       // Group conflicts
-                                       groupConflictMap[ group ] = groupData.conflicts;
-                               }
-
-                               groupData.filters.forEach( function ( itemData ) {
-                                       var filterItem = model.groups[ group ].getItemByParamName( itemData.name );
-                                       // Filter conflicts
-                                       if ( itemData.conflicts ) {
-                                               filterConflictMap[ filterItem.getName() ] = itemData.conflicts;
-                                       }
-                               } );
-                       } );
-               } );
-
-               // Add item references to the model, for lookup
-               this.addItems( items );
-
-               // Expand conflicts
-               groupConflictResult = expandConflictDefinitions( groupConflictMap );
-               filterConflictResult = expandConflictDefinitions( filterConflictMap );
-
-               // Set conflicts for groups
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( groupConflictResult, function ( group, conflicts ) {
-                       model.groups[ group ].setConflicts( conflicts );
-               } );
-
-               // Set conflicts for items
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( filterConflictResult, function ( filterName, conflicts ) {
-                       var filterItem = model.getItemByName( filterName );
-                       // set conflicts for items in the group
-                       filterItem.setConflicts( conflicts );
-               } );
-
-               // Create a map between known parameters and their models
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.groups, function ( group, groupModel ) {
-                       if (
-                               groupModel.getType() === 'send_unselected_if_any' ||
-                               groupModel.getType() === 'boolean' ||
-                               groupModel.getType() === 'any_value'
-                       ) {
-                               // Individual filters
-                               groupModel.getItems().forEach( function ( filterItem ) {
-                                       model.parameterMap[ filterItem.getParamName() ] = filterItem;
-                               } );
-                       } else if (
-                               groupModel.getType() === 'string_options' ||
-                               groupModel.getType() === 'single_option'
-                       ) {
-                               // Group
-                               model.parameterMap[ groupModel.getName() ] = groupModel;
-                       }
-               } );
-
-               this.setSearch( '' );
-
-               this.updateHighlightedState();
-
-               // Finish initialization
-               this.emit( 'initialize' );
-       };
-
-       /**
-        * Update filter view model state based on a parameter object
-        *
-        * @param {Object} params Parameters object
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.updateStateFromParams = function ( params ) {
-               var filtersValue;
-               // For arbitrary numeric single_option values make sure the values
-               // are normalized to fit within the limits
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
-                       params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] );
-               } );
-
-               // Update filter values
-               filtersValue = this.getFiltersFromParameters( params );
-               Object.keys( filtersValue ).forEach( function ( filterName ) {
-                       this.getItemByName( filterName ).setValue( filtersValue[ filterName ] );
-               }.bind( this ) );
-
-               // Update highlight state
-               this.getItemsSupportingHighlights().forEach( function ( filterItem ) {
-                       var color = params[ filterItem.getName() + '_color' ];
-                       if ( color ) {
-                               filterItem.setHighlightColor( color );
-                       } else {
-                               filterItem.clearHighlightColor();
-                       }
-               } );
-               this.updateHighlightedState();
-
-               // Check all filter interactions
-               this.reassessFilterInteractions();
-       };
-
-       /**
-        * Get a representation of an empty (falsey) parameter state
-        *
-        * @return {Object} Empty parameter state
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getEmptyParameterState = function () {
-               if ( !this.emptyParameterState ) {
-                       this.emptyParameterState = $.extend(
-                               true,
-                               {},
-                               this.getParametersFromFilters( {} ),
-                               this.getEmptyHighlightParameters()
-                       );
-               }
-               return this.emptyParameterState;
-       };
-
-       /**
-        * Get a representation of only the non-falsey parameters
-        *
-        * @param {Object} [parameters] A given parameter state to minimize. If not given the current
-        *  state of the system will be used.
-        * @return {Object} Empty parameter state
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) {
-               var result = {};
-
-               parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
-
-               // Params
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.getEmptyParameterState(), function ( param, value ) {
-                       if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) {
-                               result[ param ] = parameters[ param ];
-                       }
-               } );
-
-               // Highlights
-               Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) {
-                       if ( parameters[ param ] ) {
-                               // If a highlight parameter is not undefined and not null
-                               // add it to the result
-                               result[ param ] = parameters[ param ];
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get a representation of the full parameter list, including all base values
-        *
-        * @return {Object} Full parameter representation
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getExpandedParamRepresentation = function () {
-               return $.extend(
-                       true,
-                       {},
-                       this.getEmptyParameterState(),
-                       this.getCurrentParameterState()
-               );
-       };
-
-       /**
-        * Get a parameter representation of the current state of the model
-        *
-        * @param {boolean} [removeStickyParams] Remove sticky filters from final result
-        * @return {Object} Parameter representation of the current state of the model
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentParameterState = function ( removeStickyParams ) {
-               var state = this.getMinimizedParamRepresentation( $.extend(
-                       true,
-                       {},
-                       this.getParametersFromFilters( this.getSelectedState() ),
-                       this.getHighlightParameters()
-               ) );
-
-               if ( removeStickyParams ) {
-                       state = this.removeStickyParams( state );
-               }
-
-               return state;
-       };
-
-       /**
-        * Delete sticky parameters from given object.
-        *
-        * @param {Object} paramState Parameter state
-        * @return {Object} Parameter state without sticky parameters
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.removeStickyParams = function ( paramState ) {
-               this.getStickyParams().forEach( function ( paramName ) {
-                       delete paramState[ paramName ];
-               } );
-
-               return paramState;
-       };
-
-       /**
-        * Turn the highlight feature on or off
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.updateHighlightedState = function () {
-               this.toggleHighlight( this.getHighlightedItems().length > 0 );
-       };
-
-       /**
-        * Get the object that defines groups by their name.
-        *
-        * @return {Object} Filter groups
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroups = function () {
-               return this.groups;
-       };
-
-       /**
-        * Get the object that defines groups that match a certain view by their name.
-        *
-        * @param {string} [view] Requested view. If not given, uses current view
-        * @return {Object} Filter groups matching a display group
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
-               var result = {};
-
-               view = view || this.getCurrentView();
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.groups, function ( groupName, groupModel ) {
-                       if ( groupModel.getView() === view ) {
-                               result[ groupName ] = groupModel;
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get an array of filters matching the given display group.
-        *
-        * @param {string} [view] Requested view. If not given, uses current view
-        * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersByView = function ( view ) {
-               var groups,
-                       result = [];
-
-               view = view || this.getCurrentView();
-
-               groups = this.getFilterGroupsByView( view );
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( groups, function ( groupName, groupModel ) {
-                       result = result.concat( groupModel.getItems() );
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get the trigger for the requested view.
-        *
-        * @param {string} view View name
-        * @return {string} View trigger, if exists
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
-               return ( this.views[ view ] && this.views[ view ].trigger ) || '';
-       };
-
-       /**
-        * Get the value of a specific parameter
-        *
-        * @param {string} name Parameter name
-        * @return {number|string} Parameter value
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getParamValue = function ( name ) {
-               return this.parameters[ name ];
-       };
-
-       /**
-        * Get the current selected state of the filters
-        *
-        * @param {boolean} [onlySelected] return an object containing only the filters with a value
-        * @return {Object} Filters selected state
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function ( onlySelected ) {
-               var i,
-                       items = this.getItems(),
-                       result = {};
-
-               for ( i = 0; i < items.length; i++ ) {
-                       if ( !onlySelected || items[ i ].getValue() ) {
-                               result[ items[ i ].getName() ] = items[ i ].getValue();
-                       }
-               }
-
-               return result;
-       };
-
-       /**
-        * Get the current full state of the filters
-        *
-        * @return {Object} Filters full state
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFullState = function () {
-               var i,
-                       items = this.getItems(),
-                       result = {};
-
-               for ( i = 0; i < items.length; i++ ) {
-                       result[ items[ i ].getName() ] = {
-                               selected: items[ i ].isSelected(),
-                               conflicted: items[ i ].isConflicted(),
-                               included: items[ i ].isIncluded()
-                       };
-               }
-
-               return result;
-       };
-
-       /**
-        * Get an object representing default parameters state
-        *
-        * @return {Object} Default parameter values
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
-               var result = {};
-
-               // Get default filter state
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.groups, function ( name, model ) {
-                       if ( !model.isSticky() ) {
-                               $.extend( true, result, model.getDefaultParams() );
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get a parameter representation of all sticky parameters
-        *
-        * @return {Object} Sticky parameter values
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () {
-               var result = [];
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.groups, function ( name, model ) {
-                       if ( model.isSticky() ) {
-                               if ( model.isPerGroupRequestParameter() ) {
-                                       result.push( name );
-                               } else {
-                                       // Each filter is its own param
-                                       result = result.concat( model.getItems().map( function ( filterItem ) {
-                                               return filterItem.getParamName();
-                                       } ) );
-                               }
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get a parameter representation of all sticky parameters
-        *
-        * @return {Object} Sticky parameter values
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParamsValues = function () {
-               var result = {};
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.groups, function ( name, model ) {
-                       if ( model.isSticky() ) {
-                               $.extend( true, result, model.getParamRepresentation() );
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Analyze the groups and their filters and output an object representing
-        * the state of the parameters they represent.
-        *
-        * @param {Object} [filterDefinition] An object defining the filter values,
-        *  keyed by filter names.
-        * @return {Object} Parameter state object
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterDefinition ) {
-               var groupItemDefinition,
-                       result = {},
-                       groupItems = this.getFilterGroups();
-
-               if ( filterDefinition ) {
-                       groupItemDefinition = {};
-                       // Filter definition is "flat", but in effect
-                       // each group needs to tell us its result based
-                       // on the values in it. We need to split this list
-                       // back into groupings so we can "feed" it to the
-                       // loop below, and we need to expand it so it includes
-                       // all filters (set to false)
-                       this.getItems().forEach( function ( filterItem ) {
-                               groupItemDefinition[ filterItem.getGroupName() ] = groupItemDefinition[ filterItem.getGroupName() ] || {};
-                               groupItemDefinition[ filterItem.getGroupName() ][ filterItem.getName() ] = filterItem.coerceValue( filterDefinition[ filterItem.getName() ] );
-                       } );
-               }
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( groupItems, function ( group, model ) {
-                       $.extend(
-                               result,
-                               model.getParamRepresentation(
-                                       groupItemDefinition ?
-                                               groupItemDefinition[ group ] : null
-                               )
-                       );
-               } );
-
-               return result;
-       };
-
-       /**
-        * This is the opposite of the #getParametersFromFilters method; this goes over
-        * the given parameters and translates into a selected/unselected value in the filters.
-        *
-        * @param {Object} params Parameters query object
-        * @return {Object} Filter state object
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
-               var groupMap = {},
-                       model = this,
-                       result = {};
-
-               // Go over the given parameters, break apart to groupings
-               // The resulting object represents the group with its parameter
-               // values. For example:
-               // {
-               //    group1: {
-               //       param1: "1",
-               //       param2: "0",
-               //       param3: "1"
-               //    },
-               //    group2: "param4|param5"
-               // }
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( params, function ( paramName, paramValue ) {
-                       var groupName,
-                               itemOrGroup = model.parameterMap[ paramName ];
-
-                       if ( itemOrGroup ) {
-                               groupName = itemOrGroup instanceof mw.rcfilters.dm.FilterItem ?
-                                       itemOrGroup.getGroupName() : itemOrGroup.getName();
-
-                               groupMap[ groupName ] = groupMap[ groupName ] || {};
-                               groupMap[ groupName ][ paramName ] = paramValue;
-                       }
-               } );
-
-               // Go over all groups, so we make sure we get the complete output
-               // even if the parameters don't include a certain group
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.groups, function ( groupName, groupModel ) {
-                       result = $.extend( true, {}, result, groupModel.getFilterRepresentation( groupMap[ groupName ] ) );
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get the highlight parameters based on current filter configuration
-        *
-        * @return {Object} Object where keys are `<filter name>_color` and values
-        *                  are the selected highlight colors.
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
-               var highlightEnabled = this.isHighlightEnabled(),
-                       result = {};
-
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isHighlightSupported() ) {
-                               result[ filterItem.getName() + '_color' ] = highlightEnabled && filterItem.isHighlighted() ?
-                                       filterItem.getHighlightColor() :
-                                       null;
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get an object representing the complete empty state of highlights
-        *
-        * @return {Object} Object containing all the highlight parameters set to their negative value
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getEmptyHighlightParameters = function () {
-               var result = {};
-
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( filterItem.isHighlightSupported() ) {
-                               result[ filterItem.getName() + '_color' ] = null;
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Get an array of currently applied highlight colors
-        *
-        * @return {string[]} Currently applied highlight colors
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
-               var result = [];
-
-               if ( this.isHighlightEnabled() ) {
-                       this.getHighlightedItems().forEach( function ( filterItem ) {
-                               var color = filterItem.getHighlightColor();
-
-                               if ( result.indexOf( color ) === -1 ) {
-                                       result.push( color );
-                               }
-                       } );
-               }
-
-               return result;
-       };
-
-       /**
-        * Sanitize value group of a string_option groups type
-        * Remove duplicates and make sure to only use valid
-        * values.
-        *
-        * @private
-        * @param {string} groupName Group name
-        * @param {string[]} valueArray Array of values
-        * @return {string[]} Array of valid values
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function ( groupName, valueArray ) {
-               var validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
-                       return filterItem.getParamName();
-               } );
-
-               return mw.rcfilters.utils.normalizeParamOptions( valueArray, validNames );
-       };
-
-       /**
-        * Check whether no visible filter is selected.
-        *
-        * Filter groups that are hidden or sticky are not shown in the
-        * active filters area and therefore not included in this check.
-        *
-        * @return {boolean} No visible filter is selected
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.areVisibleFiltersEmpty = function () {
-               // Check if there are either any selected items or any items
-               // that have highlight enabled
-               return !this.getItems().some( function ( filterItem ) {
-                       var visible = !filterItem.getGroupModel().isSticky() && !filterItem.getGroupModel().isHidden(),
-                               active = ( filterItem.isSelected() || filterItem.isHighlighted() );
-                       return visible && active;
-               } );
-       };
-
-       /**
-        * Check whether the invert state is a valid one. A valid invert state is one where
-        * there are actual namespaces selected.
-        *
-        * This is done to compare states to previous ones that may have had the invert model
-        * selected but effectively had no namespaces, so are not effectively different than
-        * ones where invert is not selected.
-        *
-        * @return {boolean} Invert is effectively selected
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesEffectivelyInverted = function () {
-               return this.getInvertModel().isSelected() &&
-                       this.findSelectedItems().some( function ( itemModel ) {
-                               return itemModel.getGroupModel().getName() === 'namespace';
-                       } );
-       };
-
-       /**
-        * Get the item that matches the given name
-        *
-        * @param {string} name Filter name
-        * @return {mw.rcfilters.dm.FilterItem} Filter item
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getItemByName = function ( name ) {
-               return this.getItems().filter( function ( item ) {
-                       return name === item.getName();
-               } )[ 0 ];
-       };
-
-       /**
-        * Set all filters to false or empty/all
-        * This is equivalent to display all.
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () {
-               this.getItems().forEach( function ( filterItem ) {
-                       if ( !filterItem.getGroupModel().isSticky() ) {
-                               this.toggleFilterSelected( filterItem.getName(), false );
-                       }
-               }.bind( this ) );
-       };
-
-       /**
-        * Toggle selected state of one item
-        *
-        * @param {string} name Name of the filter item
-        * @param {boolean} [isSelected] Filter selected state
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
-               var item = this.getItemByName( name );
-
-               if ( item ) {
-                       item.toggleSelected( isSelected );
-               }
-       };
-
-       /**
-        * Toggle selected state of items by their names
-        *
-        * @param {Object} filterDef Filter definitions
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) {
-               Object.keys( filterDef ).forEach( function ( name ) {
-                       this.toggleFilterSelected( name, filterDef[ name ] );
-               }.bind( this ) );
-       };
-
-       /**
-        * Get a group model from its name
-        *
-        * @param {string} groupName Group name
-        * @return {mw.rcfilters.dm.FilterGroup} Group model
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getGroup = function ( groupName ) {
-               return this.groups[ groupName ];
-       };
-
-       /**
-        * Get all filters within a specified group by its name
-        *
-        * @param {string} groupName Group name
-        * @return {mw.rcfilters.dm.FilterItem[]} Filters belonging to this group
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getGroupFilters = function ( groupName ) {
-               return ( this.getGroup( groupName ) && this.getGroup( groupName ).getItems() ) || [];
-       };
-
-       /**
-        * Find items whose labels match the given string
-        *
-        * @param {string} query Search string
-        * @param {boolean} [returnFlat] Return a flat array. If false, the result
-        *  is an object whose keys are the group names and values are an array of
-        *  filters per group. If set to true, returns an array of filters regardless
-        *  of their groups.
-        * @return {Object} An object of items to show
-        *  arranged by their group names
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
-               var i, searchIsEmpty,
-                       groupTitle,
-                       result = {},
-                       flatResult = [],
-                       view = this.getViewByTrigger( query.substr( 0, 1 ) ),
-                       items = this.getFiltersByView( view );
-
-               // Normalize so we can search strings regardless of case and view
-               query = query.trim().toLowerCase();
-               if ( view !== 'default' ) {
-                       query = query.substr( 1 );
-               }
-               // Trim again to also intercept cases where the spaces were after the trigger
-               // eg: '#   str'
-               query = query.trim();
-
-               // Check if the search if actually empty; this can be a problem when
-               // we use prefixes to denote different views
-               searchIsEmpty = query.length === 0;
-
-               // item label starting with the query string
-               for ( i = 0; i < items.length; i++ ) {
-                       if (
-                               searchIsEmpty ||
-                               items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
-                               (
-                                       // For tags, we want the parameter name to be included in the search
-                                       view === 'tags' &&
-                                       items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
-                               )
-                       ) {
-                               result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
-                               result[ items[ i ].getGroupName() ].push( items[ i ] );
-                               flatResult.push( items[ i ] );
-                       }
-               }
-
-               if ( $.isEmptyObject( result ) ) {
-                       // item containing the query string in their label, description, or group title
-                       for ( i = 0; i < items.length; i++ ) {
-                               groupTitle = items[ i ].getGroupModel().getTitle();
-                               if (
-                                       searchIsEmpty ||
-                                       items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
-                                       items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
-                                       groupTitle.toLowerCase().indexOf( query ) > -1 ||
-                                       (
-                                               // For tags, we want the parameter name to be included in the search
-                                               view === 'tags' &&
-                                               items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
-                                       )
-                               ) {
-                                       result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
-                                       result[ items[ i ].getGroupName() ].push( items[ i ] );
-                                       flatResult.push( items[ i ] );
-                               }
-                       }
-               }
-
-               return returnFlat ? flatResult : result;
-       };
-
-       /**
-        * Get items that are highlighted
-        *
-        * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
-               return this.getItems().filter( function ( filterItem ) {
-                       return filterItem.isHighlightSupported() &&
-                               filterItem.getHighlightColor();
-               } );
-       };
-
-       /**
-        * Get items that allow highlights even if they're not currently highlighted
-        *
-        * @return {mw.rcfilters.dm.FilterItem[]} Items supporting highlights
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getItemsSupportingHighlights = function () {
-               return this.getItems().filter( function ( filterItem ) {
-                       return filterItem.isHighlightSupported();
-               } );
-       };
-
-       /**
-        * Get all selected items
-        *
-        * @return {mw.rcfilters.dm.FilterItem[]} Selected items
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.findSelectedItems = function () {
-               var allSelected = [];
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
-                       allSelected = allSelected.concat( groupModel.findSelectedItems() );
-               } );
-
-               return allSelected;
-       };
-
-       /**
-        * Get the current view
-        *
-        * @return {string} Current view
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentView = function () {
-               return this.currentView;
-       };
-
-       /**
-        * Get the label for the current view
-        *
-        * @param {string} viewName View name
-        * @return {string} Label for the current view
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getViewTitle = function ( viewName ) {
-               viewName = viewName || this.getCurrentView();
-
-               return this.views[ viewName ] && this.views[ viewName ].title;
-       };
-
-       /**
-        * Get the view that fits the given trigger
-        *
-        * @param {string} trigger Trigger
-        * @return {string} Name of view
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
-               var result = 'default';
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( this.views, function ( name, data ) {
-                       if ( data.trigger === trigger ) {
-                               result = name;
-                       }
-               } );
-
-               return result;
-       };
-
-       /**
-        * Return a version of the given string that is without any
-        * view triggers.
-        *
-        * @param {string} str Given string
-        * @return {string} Result
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.removeViewTriggers = function ( str ) {
-               if ( this.getViewFromString( str ) !== 'default' ) {
-                       str = str.substr( 1 );
-               }
-
-               return str;
-       };
-
-       /**
-        * Get the view from the given string by a trigger, if it exists
-        *
-        * @param {string} str Given string
-        * @return {string} View name
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getViewFromString = function ( str ) {
-               return this.getViewByTrigger( str.substr( 0, 1 ) );
-       };
-
-       /**
-        * Set the current search for the system.
-        * This also dictates what items and groups are visible according
-        * to the search in #findMatches
-        *
-        * @param {string} searchQuery Search query, including triggers
-        * @fires searchChange
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.setSearch = function ( searchQuery ) {
-               var visibleGroups, visibleGroupNames;
-
-               if ( this.searchQuery !== searchQuery ) {
-                       // Check if the view changed
-                       this.switchView( this.getViewFromString( searchQuery ) );
-
-                       visibleGroups = this.findMatches( searchQuery );
-                       visibleGroupNames = Object.keys( visibleGroups );
-
-                       // Update visibility of items and groups
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
-                               // Check if the group is visible at all
-                               groupModel.toggleVisible( visibleGroupNames.indexOf( groupName ) !== -1 );
-                               groupModel.setVisibleItems( visibleGroups[ groupName ] || [] );
-                       } );
-
-                       this.searchQuery = searchQuery;
-                       this.emit( 'searchChange', this.searchQuery );
-               }
-       };
-
-       /**
-        * Get the current search
-        *
-        * @return {string} Current search query
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getSearch = function () {
-               return this.searchQuery;
-       };
-
-       /**
-        * Switch the current view
-        *
-        * @private
-        * @param {string} view View name
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.switchView = function ( view ) {
-               if ( this.views[ view ] && this.currentView !== view ) {
-                       this.currentView = view;
-               }
-       };
-
-       /**
-        * Toggle the highlight feature on and off.
-        * Propagate the change to filter items.
-        *
-        * @param {boolean} enable Highlight should be enabled
-        * @fires highlightChange
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
-               enable = enable === undefined ? !this.highlightEnabled : enable;
-
-               if ( this.highlightEnabled !== enable ) {
-                       this.highlightEnabled = enable;
-                       this.emit( 'highlightChange', this.highlightEnabled );
-               }
-       };
-
-       /**
-        * Check if the highlight feature is enabled
-        * @return {boolean}
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
-               return !!this.highlightEnabled;
-       };
-
-       /**
-        * Toggle the inverted namespaces property on and off.
-        * Propagate the change to namespace filter items.
-        *
-        * @param {boolean} enable Inverted property is enabled
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
-               this.toggleFilterSelected( this.getInvertModel().getName(), enable );
-       };
-
-       /**
-        * Get the model object that represents the 'invert' filter
-        *
-        * @return {mw.rcfilters.dm.FilterItem}
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.getInvertModel = function () {
-               return this.getGroup( 'invertGroup' ).getItemByParamName( 'invert' );
-       };
-
-       /**
-        * Set highlight color for a specific filter item
-        *
-        * @param {string} filterName Name of the filter item
-        * @param {string} color Selected color
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
-               this.getItemByName( filterName ).setHighlightColor( color );
-       };
-
-       /**
-        * Clear highlight for a specific filter item
-        *
-        * @param {string} filterName Name of the filter item
-        */
-       mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
-               this.getItemByName( filterName ).clearHighlightColor();
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
deleted file mode 100644 (file)
index c3283c1..0000000
+++ /dev/null
@@ -1,273 +0,0 @@
-( function () {
-       /**
-        * RCFilter base item model
-        *
-        * @mixins OO.EventEmitter
-        *
-        * @constructor
-        * @param {string} param Filter param name
-        * @param {Object} config Configuration object
-        * @cfg {string} [label] The label for the filter
-        * @cfg {string} [description] The description of the filter
-        * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
-        *  group. If the prefix has 'invert' state, the parameter is expected to be an object
-        *  with 'default' and 'inverted' as keys.
-        * @cfg {boolean} [active=true] The filter is active and affecting the result
-        * @cfg {boolean} [selected] The item is selected
-        * @cfg {*} [value] The value of this item
-        * @cfg {string} [namePrefix='item_'] A prefix to add to the param name to act as a unique
-        *  identifier
-        * @cfg {string} [cssClass] The class identifying the results that match this filter
-        * @cfg {string[]} [identifiers] An array of identifiers for this item. They will be
-        *  added and considered in the view.
-        * @cfg {string} [defaultHighlightColor=null] If set, highlight this filter by default with this color
-        */
-       mw.rcfilters.dm.ItemModel = function MwRcfiltersDmItemModel( param, config ) {
-               config = config || {};
-
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-
-               this.param = param;
-               this.namePrefix = config.namePrefix || 'item_';
-               this.name = this.namePrefix + param;
-
-               this.label = config.label || this.name;
-               this.labelPrefixKey = config.labelPrefixKey;
-               this.description = config.description || '';
-               this.setValue( config.value || config.selected );
-
-               this.identifiers = config.identifiers || [];
-
-               // Highlight
-               this.cssClass = config.cssClass;
-               this.highlightColor = config.defaultHighlightColor || null;
-       };
-
-       /* Initialization */
-
-       OO.initClass( mw.rcfilters.dm.ItemModel );
-       OO.mixinClass( mw.rcfilters.dm.ItemModel, OO.EventEmitter );
-
-       /* Events */
-
-       /**
-        * @event update
-        *
-        * The state of this filter has changed
-        */
-
-       /* Methods */
-
-       /**
-        * Return the representation of the state of this item.
-        *
-        * @return {Object} State of the object
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getState = function () {
-               return {
-                       selected: this.isSelected()
-               };
-       };
-
-       /**
-        * Get the name of this filter
-        *
-        * @return {string} Filter name
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getName = function () {
-               return this.name;
-       };
-
-       /**
-        * Get the message key to use to wrap the label. This message takes the label as a parameter.
-        *
-        * @param {boolean} inverted Whether this item should be considered inverted
-        * @return {string|null} Message key, or null if no message
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getLabelMessageKey = function ( inverted ) {
-               if ( this.labelPrefixKey ) {
-                       if ( typeof this.labelPrefixKey === 'string' ) {
-                               return this.labelPrefixKey;
-                       }
-                       return this.labelPrefixKey[
-                               // Only use inverted-prefix if the item is selected
-                               // Highlight-only an inverted item makes no sense
-                               inverted && this.isSelected() ?
-                                       'inverted' : 'default'
-                       ];
-               }
-               return null;
-       };
-
-       /**
-        * Get the param name or value of this filter
-        *
-        * @return {string} Filter param name
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getParamName = function () {
-               return this.param;
-       };
-
-       /**
-        * Get the message representing the state of this model.
-        *
-        * @return {string} State message
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getStateMessage = function () {
-               // Display description
-               return this.getDescription();
-       };
-
-       /**
-        * Get the label of this filter
-        *
-        * @return {string} Filter label
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getLabel = function () {
-               return this.label;
-       };
-
-       /**
-        * Get the description of this filter
-        *
-        * @return {string} Filter description
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getDescription = function () {
-               return this.description;
-       };
-
-       /**
-        * Get the default value of this filter
-        *
-        * @return {boolean} Filter default
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getDefault = function () {
-               return this.default;
-       };
-
-       /**
-        * Get the selected state of this filter
-        *
-        * @return {boolean} Filter is selected
-        */
-       mw.rcfilters.dm.ItemModel.prototype.isSelected = function () {
-               return !!this.value;
-       };
-
-       /**
-        * Toggle the selected state of the item
-        *
-        * @param {boolean} [isSelected] Filter is selected
-        * @fires update
-        */
-       mw.rcfilters.dm.ItemModel.prototype.toggleSelected = function ( isSelected ) {
-               isSelected = isSelected === undefined ? !this.isSelected() : isSelected;
-               this.setValue( isSelected );
-       };
-
-       /**
-        * Get the value
-        *
-        * @return {*}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getValue = function () {
-               return this.value;
-       };
-
-       /**
-        * Convert a given value to the appropriate representation based on group type
-        *
-        * @param {*} value
-        * @return {*}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.coerceValue = function ( value ) {
-               return this.getGroupModel().getType() === 'any_value' ? value : !!value;
-       };
-
-       /**
-        * Set the value
-        *
-        * @param {*} newValue
-        */
-       mw.rcfilters.dm.ItemModel.prototype.setValue = function ( newValue ) {
-               newValue = this.coerceValue( newValue );
-               if ( this.value !== newValue ) {
-                       this.value = newValue;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Set the highlight color
-        *
-        * @param {string|null} highlightColor
-        */
-       mw.rcfilters.dm.ItemModel.prototype.setHighlightColor = function ( highlightColor ) {
-               if ( !this.isHighlightSupported() ) {
-                       return;
-               }
-               // If the highlight color on the item and in the parameter is null/undefined, return early.
-               if ( !this.highlightColor && !highlightColor ) {
-                       return;
-               }
-
-               if ( this.highlightColor !== highlightColor ) {
-                       this.highlightColor = highlightColor;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Clear the highlight color
-        */
-       mw.rcfilters.dm.ItemModel.prototype.clearHighlightColor = function () {
-               this.setHighlightColor( null );
-       };
-
-       /**
-        * Get the highlight color, or null if none is configured
-        *
-        * @return {string|null}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getHighlightColor = function () {
-               return this.highlightColor;
-       };
-
-       /**
-        * Get the CSS class that matches changes that fit this filter
-        * or null if none is configured
-        *
-        * @return {string|null}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getCssClass = function () {
-               return this.cssClass;
-       };
-
-       /**
-        * Get the item's identifiers
-        *
-        * @return {string[]}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.getIdentifiers = function () {
-               return this.identifiers;
-       };
-
-       /**
-        * Check if the highlight feature is supported for this filter
-        *
-        * @return {boolean}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.isHighlightSupported = function () {
-               return !!this.getCssClass();
-       };
-
-       /**
-        * Check if the filter is currently highlighted
-        *
-        * @return {boolean}
-        */
-       mw.rcfilters.dm.ItemModel.prototype.isHighlighted = function () {
-               return !!this.getHighlightColor();
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
deleted file mode 100644 (file)
index adf3fbb..0000000
+++ /dev/null
@@ -1,410 +0,0 @@
-( function () {
-       /**
-        * View model for saved queries
-        *
-        * @class
-        * @mixins OO.EventEmitter
-        * @mixins OO.EmitterList
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
-        * @param {Object} [config] Configuration options
-        * @cfg {string} [default] Default query ID
-        */
-       mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
-               config = config || {};
-
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-               OO.EmitterList.call( this );
-
-               this.default = config.default;
-               this.filtersModel = filtersModel;
-               this.converted = false;
-
-               // Events
-               this.aggregate( { update: 'itemUpdate' } );
-       };
-
-       /* Initialization */
-
-       OO.initClass( mw.rcfilters.dm.SavedQueriesModel );
-       OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EventEmitter );
-       OO.mixinClass( mw.rcfilters.dm.SavedQueriesModel, OO.EmitterList );
-
-       /* Events */
-
-       /**
-        * @event initialize
-        *
-        * Model is initialized
-        */
-
-       /**
-        * @event itemUpdate
-        * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
-        *
-        * An item has changed
-        */
-
-       /**
-        * @event default
-        * @param {string} New default ID
-        *
-        * The default has changed
-        */
-
-       /* Methods */
-
-       /**
-        * Initialize the saved queries model by reading it from the user's settings.
-        * The structure of the saved queries is:
-        * {
-        *    version: (string) Version number; if version 2, the query represents
-        *             parameters. Otherwise, the older version represented filters
-        *             and needs to be readjusted,
-        *    default: (string) Query ID
-        *    queries:{
-        *       query_id_1: {
-        *          data:{
-        *             filters: (Object) Minimal definition of the filters
-        *             highlights: (Object) Definition of the highlights
-        *          },
-        *          label: (optional) Name of this query
-        *       }
-        *    }
-        * }
-        *
-        * @param {Object} [savedQueries] An object with the saved queries with
-        *  the above structure.
-        * @fires initialize
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
-               var model = this;
-
-               savedQueries = savedQueries || {};
-
-               this.clearItems();
-               this.default = null;
-               this.converted = false;
-
-               if ( savedQueries.version !== '2' ) {
-                       // Old version dealt with filter names. We need to migrate to the new structure
-                       // The new structure:
-                       // {
-                       //   version: (string) '2',
-                       //   default: (string) Query ID,
-                       //   queries: {
-                       //     query_id: {
-                       //       label: (string) Name of the query
-                       //       data: {
-                       //         params: (object) Representing all the parameter states
-                       //         highlights: (object) Representing all the filter highlight states
-                       //     }
-                       //   }
-                       // }
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( savedQueries.queries || {}, function ( id, obj ) {
-                               if ( obj.data && obj.data.filters ) {
-                                       obj.data = model.convertToParameters( obj.data );
-                               }
-                       } );
-
-                       this.converted = true;
-                       savedQueries.version = '2';
-               }
-
-               // Initialize the query items
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( savedQueries.queries || {}, function ( id, obj ) {
-                       var normalizedData = obj.data,
-                               isDefault = String( savedQueries.default ) === String( id );
-
-                       if ( normalizedData && normalizedData.params ) {
-                               // Backwards-compat fix: Remove sticky parameters from
-                               // the given data, if they exist
-                               normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );
-
-                               // Correct the invert state for effective selection
-                               if ( normalizedData.params.invert && !normalizedData.params.namespace ) {
-                                       delete normalizedData.params.invert;
-                               }
-
-                               model.cleanupHighlights( normalizedData );
-
-                               id = String( id );
-
-                               // Skip the addNewQuery method because we don't want to unnecessarily manipulate
-                               // the given saved queries unless we literally intend to (like in backwards compat fixes)
-                               // And the addNewQuery method also uses a minimization routine that checks for the
-                               // validity of items and minimizes the query. This isn't necessary for queries loaded
-                               // from the backend, and has the risk of removing values if they're temporarily
-                               // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
-                               model.addItems( [
-                                       new mw.rcfilters.dm.SavedQueryItemModel(
-                                               id,
-                                               obj.label,
-                                               normalizedData,
-                                               { default: isDefault }
-                                       )
-                               ] );
-
-                               if ( isDefault ) {
-                                       model.default = id;
-                               }
-                       }
-               } );
-
-               this.emit( 'initialize' );
-       };
-
-       /**
-        * Clean up highlight parameters.
-        * 'highlight' used to be stored, it's not inferred based on the presence of absence of
-        * filter colors.
-        *
-        * @param {Object} data Saved query data
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.cleanupHighlights = function ( data ) {
-               if (
-                       data.params.highlight === '0' &&
-                       data.highlights && Object.keys( data.highlights ).length
-               ) {
-                       data.highlights = {};
-               }
-               delete data.params.highlight;
-       };
-
-       /**
-        * Convert from representation of filters to representation of parameters
-        *
-        * @param {Object} data Query data
-        * @return {Object} New converted query data
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.convertToParameters = function ( data ) {
-               var newData = {},
-                       defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
-                       fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
-                       highlightEnabled = data.highlights.highlight;
-
-               delete data.highlights.highlight;
-
-               // Filters
-               newData.params = this.filtersModel.getMinimizedParamRepresentation(
-                       this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
-               );
-
-               // Highlights: appending _color to keys
-               newData.highlights = {};
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( data.highlights, function ( highlightedFilterName, value ) {
-                       if ( value ) {
-                               newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
-                       }
-               } );
-
-               // Add highlight
-               newData.params.highlight = String( Number( highlightEnabled || 0 ) );
-
-               return newData;
-       };
-
-       /**
-        * Add a query item
-        *
-        * @param {string} label Label for the new query
-        * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
-        * @param {boolean} isDefault Item is default
-        * @param {string} [id] Query ID, if exists. If this isn't given, a random
-        *  new ID will be created.
-        * @return {string} ID of the newly added query
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
-               var normalizedData = { params: {}, highlights: {} },
-                       highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
-                       randomID = String( id || ( new Date() ).getTime() ),
-                       data = this.filtersModel.getMinimizedParamRepresentation( fulldata );
-
-               // Split highlight/params
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( data, function ( param, value ) {
-                       if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
-                               normalizedData.highlights[ param ] = value;
-                       } else {
-                               normalizedData.params[ param ] = value;
-                       }
-               } );
-
-               // Correct the invert state for effective selection
-               if ( normalizedData.params.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
-                       delete normalizedData.params.invert;
-               }
-
-               // Add item
-               this.addItems( [
-                       new mw.rcfilters.dm.SavedQueryItemModel(
-                               randomID,
-                               label,
-                               normalizedData,
-                               { default: isDefault }
-                       )
-               ] );
-
-               if ( isDefault ) {
-                       this.setDefault( randomID );
-               }
-
-               return randomID;
-       };
-
-       /**
-        * Remove query from model
-        *
-        * @param {string} queryID Query ID
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
-               var query = this.getItemByID( queryID );
-
-               if ( query ) {
-                       // Check if this item was the default
-                       if ( String( this.getDefault() ) === String( queryID ) ) {
-                               // Nulify the default
-                               this.setDefault( null );
-                       }
-
-                       this.removeItems( [ query ] );
-               }
-       };
-
-       /**
-        * Get an item that matches the requested query
-        *
-        * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
-        * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
-               // Minimize before comparison
-               fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );
-
-               // Correct the invert state for effective selection
-               if ( fullQueryComparison.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
-                       delete fullQueryComparison.invert;
-               }
-
-               return this.getItems().filter( function ( item ) {
-                       return OO.compare(
-                               item.getCombinedData(),
-                               fullQueryComparison
-                       );
-               } )[ 0 ];
-       };
-
-       /**
-        * Get query by its identifier
-        *
-        * @param {string} queryID Query identifier
-        * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
-        *  the search. Undefined if not found.
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
-               return this.getItems().filter( function ( item ) {
-                       return item.getID() === queryID;
-               } )[ 0 ];
-       };
-
-       /**
-        * Get the full data representation of the default query, if it exists
-        *
-        * @return {Object|null} Representation of the default params if exists.
-        *  Null if default doesn't exist or if the user is not logged in.
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.getDefaultParams = function () {
-               return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
-       };
-
-       /**
-        * Get a full parameter representation of an item data
-        *
-        * @param  {Object} queryID Query ID
-        * @return {Object} Parameter representation
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
-               var item = this.getItemByID( queryID ),
-                       data = item ? item.getData() : {};
-
-               return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
-       };
-
-       /**
-        * Build a full parameter representation given item data and model sticky values state
-        *
-        * @param  {Object} data Item data
-        * @return {Object} Full param representation
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
-               data = data || {};
-               // Return parameter representation
-               return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
-                       data.params,
-                       data.highlights
-               ) );
-       };
-
-       /**
-        * Get the object representing the state of the entire model and items
-        *
-        * @return {Object} Object representing the state of the model and items
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () {
-               var obj = { queries: {}, version: '2' };
-
-               // Translate the items to the saved object
-               this.getItems().forEach( function ( item ) {
-                       obj.queries[ item.getID() ] = item.getState();
-               } );
-
-               if ( this.getDefault() ) {
-                       obj.default = this.getDefault();
-               }
-
-               return obj;
-       };
-
-       /**
-        * Set a default query. Null to unset default.
-        *
-        * @param {string} itemID Query identifier
-        * @fires default
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.setDefault = function ( itemID ) {
-               if ( this.default !== itemID ) {
-                       this.default = itemID;
-
-                       // Set for individual itens
-                       this.getItems().forEach( function ( item ) {
-                               item.toggleDefault( item.getID() === itemID );
-                       } );
-
-                       this.emit( 'default', itemID );
-               }
-       };
-
-       /**
-        * Get the default query ID
-        *
-        * @return {string} Default query identifier
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.getDefault = function () {
-               return this.default;
-       };
-
-       /**
-        * Check if the saved queries were converted
-        *
-        * @return {boolean} Saved queries were converted from the previous
-        *  version to the new version
-        */
-       mw.rcfilters.dm.SavedQueriesModel.prototype.isConverted = function () {
-               return this.converted;
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js
deleted file mode 100644 (file)
index 46344cb..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-( function () {
-       /**
-        * View model for a single saved query
-        *
-        * @class
-        * @mixins OO.EventEmitter
-        *
-        * @constructor
-        * @param {string} id Unique identifier
-        * @param {string} label Saved query label
-        * @param {Object} data Saved query data
-        * @param {Object} [config] Configuration options
-        * @cfg {boolean} [default] This item is the default
-        */
-       mw.rcfilters.dm.SavedQueryItemModel = function MwRcfiltersDmSavedQueriesModel( id, label, data, config ) {
-               config = config || {};
-
-               // Mixin constructor
-               OO.EventEmitter.call( this );
-
-               this.id = id;
-               this.label = label;
-               this.data = data;
-               this.default = !!config.default;
-       };
-
-       /* Initialization */
-
-       OO.initClass( mw.rcfilters.dm.SavedQueryItemModel );
-       OO.mixinClass( mw.rcfilters.dm.SavedQueryItemModel, OO.EventEmitter );
-
-       /* Events */
-
-       /**
-        * @event update
-        *
-        * Model has been updated
-        */
-
-       /* Methods */
-
-       /**
-        * Get an object representing the state of this item
-        *
-        * @return {Object} Object representing the current data state
-        *  of the object
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.getState = function () {
-               return {
-                       data: this.getData(),
-                       label: this.getLabel()
-               };
-       };
-
-       /**
-        * Get the query's identifier
-        *
-        * @return {string} Query identifier
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.getID = function () {
-               return this.id;
-       };
-
-       /**
-        * Get query label
-        *
-        * @return {string} Query label
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.getLabel = function () {
-               return this.label;
-       };
-
-       /**
-        * Update the query label
-        *
-        * @param {string} newLabel New label
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.updateLabel = function ( newLabel ) {
-               if ( newLabel && this.label !== newLabel ) {
-                       this.label = newLabel;
-                       this.emit( 'update' );
-               }
-       };
-
-       /**
-        * Get query data
-        *
-        * @return {Object} Object representing parameter and highlight data
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.getData = function () {
-               return this.data;
-       };
-
-       /**
-        * Get the combined data of this item as a flat object of parameters
-        *
-        * @return {Object} Combined parameter data
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.getCombinedData = function () {
-               return $.extend( true, {}, this.data.params, this.data.highlights );
-       };
-
-       /**
-        * Check whether this item is the default
-        *
-        * @return {boolean} Query is set to be default
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.isDefault = function () {
-               return this.default;
-       };
-
-       /**
-        * Toggle the default state of this query item
-        *
-        * @param {boolean} isDefault Query is default
-        */
-       mw.rcfilters.dm.SavedQueryItemModel.prototype.toggleDefault = function ( isDefault ) {
-               isDefault = isDefault === undefined ? !this.default : isDefault;
-
-               if ( this.default !== isDefault ) {
-                       this.default = isDefault;
-                       this.emit( 'update' );
-               }
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
deleted file mode 100644 (file)
index 1651432..0000000
+++ /dev/null
@@ -1,1221 +0,0 @@
-( function () {
-
-       var byteLength = require( 'mediawiki.String' ).byteLength;
-
-       /* eslint no-underscore-dangle: "off" */
-       /**
-        * Controller for the filters in Recent Changes
-        * @class
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel Changes list view model
-        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
-        * @param {Object} config Additional configuration
-        * @cfg {string} savedQueriesPreferenceName Where to save the saved queries
-        * @cfg {string} daysPreferenceName Preference name for the days filter
-        * @cfg {string} limitPreferenceName Preference name for the limit filter
-        * @cfg {string} collapsedPreferenceName Preference name for collapsing and showing
-        *  the active filters area
-        * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
-        *  title normalization to separate title subpage/parts into the target= url
-        *  parameter
-        */
-       mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel, config ) {
-               this.filtersModel = filtersModel;
-               this.changesListModel = changesListModel;
-               this.savedQueriesModel = savedQueriesModel;
-               this.savedQueriesPreferenceName = config.savedQueriesPreferenceName;
-               this.daysPreferenceName = config.daysPreferenceName;
-               this.limitPreferenceName = config.limitPreferenceName;
-               this.collapsedPreferenceName = config.collapsedPreferenceName;
-               this.normalizeTarget = !!config.normalizeTarget;
-
-               this.requestCounter = {};
-               this.baseFilterState = {};
-               this.uriProcessor = null;
-               this.initialized = false;
-               this.wereSavedQueriesSaved = false;
-
-               this.prevLoggedItems = [];
-
-               this.FILTER_CHANGE = 'filterChange';
-               this.SHOW_NEW_CHANGES = 'showNewChanges';
-               this.LIVE_UPDATE = 'liveUpdate';
-       };
-
-       /* Initialization */
-       OO.initClass( mw.rcfilters.Controller );
-
-       /**
-        * Initialize the filter and parameter states
-        *
-        * @param {Array} filterStructure Filter definition and structure for the model
-        * @param {Object} [namespaceStructure] Namespace definition
-        * @param {Object} [tagList] Tag definition
-        * @param {Object} [conditionalViews] Conditional view definition
-        */
-       mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList, conditionalViews ) {
-               var parsedSavedQueries, pieces,
-                       displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
-                       defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
-                       controller = this,
-                       views = $.extend( true, {}, conditionalViews ),
-                       items = [],
-                       uri = new mw.Uri();
-
-               // Prepare views
-               if ( namespaceStructure ) {
-                       items = [];
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( namespaceStructure, function ( namespaceID, label ) {
-                               // Build and clean up the individual namespace items definition
-                               items.push( {
-                                       name: namespaceID,
-                                       label: label || mw.msg( 'blanknamespace' ),
-                                       description: '',
-                                       identifiers: [
-                                               mw.Title.isTalkNamespace( namespaceID ) ?
-                                                       'talk' : 'subject'
-                                       ],
-                                       cssClass: 'mw-changeslist-ns-' + namespaceID
-                               } );
-                       } );
-
-                       views.namespaces = {
-                               title: mw.msg( 'namespaces' ),
-                               trigger: ':',
-                               groups: [ {
-                                       // Group definition (single group)
-                                       name: 'namespace', // parameter name is singular
-                                       type: 'string_options',
-                                       title: mw.msg( 'namespaces' ),
-                                       labelPrefixKey: { default: 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
-                                       separator: ';',
-                                       fullCoverage: true,
-                                       filters: items
-                               } ]
-                       };
-                       views.invert = {
-                               groups: [
-                                       {
-                                               name: 'invertGroup',
-                                               type: 'boolean',
-                                               hidden: true,
-                                               filters: [ {
-                                                       name: 'invert',
-                                                       default: '0'
-                                               } ]
-                                       } ]
-                       };
-               }
-               if ( tagList ) {
-                       views.tags = {
-                               title: mw.msg( 'rcfilters-view-tags' ),
-                               trigger: '#',
-                               groups: [ {
-                                       // Group definition (single group)
-                                       name: 'tagfilter', // Parameter name
-                                       type: 'string_options',
-                                       title: 'rcfilters-view-tags', // Message key
-                                       labelPrefixKey: 'rcfilters-tag-prefix-tags',
-                                       separator: '|',
-                                       fullCoverage: false,
-                                       filters: tagList
-                               } ]
-                       };
-               }
-
-               // Add parameter range operations
-               views.range = {
-                       groups: [
-                               {
-                                       name: 'limit',
-                                       type: 'single_option',
-                                       title: '', // Because it's a hidden group, this title actually appears nowhere
-                                       hidden: true,
-                                       allowArbitrary: true,
-                                       validate: $.isNumeric,
-                                       range: {
-                                               min: 0, // The server normalizes negative numbers to 0 results
-                                               max: 1000
-                                       },
-                                       sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
-                                       default: mw.user.options.get( this.limitPreferenceName, displayConfig.limitDefault ),
-                                       sticky: true,
-                                       filters: displayConfig.limitArray.map( function ( num ) {
-                                               return controller._createFilterDataFromNumber( num, num );
-                                       } )
-                               },
-                               {
-                                       name: 'days',
-                                       type: 'single_option',
-                                       title: '', // Because it's a hidden group, this title actually appears nowhere
-                                       hidden: true,
-                                       allowArbitrary: true,
-                                       validate: $.isNumeric,
-                                       range: {
-                                               min: 0,
-                                               max: displayConfig.maxDays
-                                       },
-                                       sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
-                                       numToLabelFunc: function ( i ) {
-                                               return Number( i ) < 1 ?
-                                                       ( Number( i ) * 24 ).toFixed( 2 ) :
-                                                       Number( i );
-                                       },
-                                       default: mw.user.options.get( this.daysPreferenceName, displayConfig.daysDefault ),
-                                       sticky: true,
-                                       filters: [
-                                               // Hours (1, 2, 6, 12)
-                                               0.04166, 0.0833, 0.25, 0.5
-                                       // Days
-                                       ].concat( displayConfig.daysArray )
-                                               .map( function ( num ) {
-                                                       return controller._createFilterDataFromNumber(
-                                                               num,
-                                                               // Convert fractions of days to number of hours for the labels
-                                                               num < 1 ? Math.round( num * 24 ) : num
-                                                       );
-                                               } )
-                               }
-                       ]
-               };
-
-               views.display = {
-                       groups: [
-                               {
-                                       name: 'display',
-                                       type: 'boolean',
-                                       title: '', // Because it's a hidden group, this title actually appears nowhere
-                                       hidden: true,
-                                       sticky: true,
-                                       filters: [
-                                               {
-                                                       name: 'enhanced',
-                                                       default: String( mw.user.options.get( 'usenewrc', 0 ) )
-                                               }
-                                       ]
-                               }
-                       ]
-               };
-
-               // Before we do anything, we need to see if we require additional items in the
-               // groups that have 'AllowArbitrary'. For the moment, those are only single_option
-               // groups; if we ever expand it, this might need further generalization:
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( views, function ( viewName, viewData ) {
-                       viewData.groups.forEach( function ( groupData ) {
-                               var extraValues = [];
-                               if ( groupData.allowArbitrary ) {
-                                       // If the value in the URI isn't in the group, add it
-                                       if ( uri.query[ groupData.name ] !== undefined ) {
-                                               extraValues.push( uri.query[ groupData.name ] );
-                                       }
-                                       // If the default value isn't in the group, add it
-                                       if ( groupData.default !== undefined ) {
-                                               extraValues.push( String( groupData.default ) );
-                                       }
-                                       controller.addNumberValuesToGroup( groupData, extraValues );
-                               }
-                       } );
-               } );
-
-               // Initialize the model
-               this.filtersModel.initializeFilters( filterStructure, views );
-
-               this.uriProcessor = new mw.rcfilters.UriProcessor(
-                       this.filtersModel,
-                       { normalizeTarget: this.normalizeTarget }
-               );
-
-               if ( !mw.user.isAnon() ) {
-                       try {
-                               parsedSavedQueries = JSON.parse( mw.user.options.get( this.savedQueriesPreferenceName ) || '{}' );
-                       } catch ( err ) {
-                               parsedSavedQueries = {};
-                       }
-
-                       // Initialize saved queries
-                       this.savedQueriesModel.initialize( parsedSavedQueries );
-                       if ( this.savedQueriesModel.isConverted() ) {
-                               // Since we know we converted, we're going to re-save
-                               // the queries so they are now migrated to the new format
-                               this._saveSavedQueries();
-                       }
-               }
-
-               if ( defaultSavedQueryExists ) {
-                       // This came from the server, meaning that we have a default
-                       // saved query, but the server could not load it, probably because
-                       // it was pre-conversion to the new format.
-                       // We need to load this query again
-                       this.applySavedQuery( this.savedQueriesModel.getDefault() );
-               } else {
-                       // There are either recognized parameters in the URL
-                       // or there are none, but there is also no default
-                       // saved query (so defaults are from the backend)
-                       // We want to update the state but not fetch results
-                       // again
-                       this.updateStateFromUrl( false );
-
-                       pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
-
-                       // Update the changes list with the existing data
-                       // so it gets processed
-                       this.changesListModel.update(
-                               pieces.changes,
-                               pieces.fieldset,
-                               pieces.noResultsDetails,
-                               true // We're using existing DOM elements
-                       );
-               }
-
-               this.initialized = true;
-               this.switchView( 'default' );
-
-               this.pollingRate = mw.config.get( 'StructuredChangeFiltersLiveUpdatePollingRate' );
-               if ( this.pollingRate ) {
-                       this._scheduleLiveUpdate();
-               }
-       };
-
-       /**
-        * Check if the controller has finished initializing.
-        * @return {boolean} Controller is initialized
-        */
-       mw.rcfilters.Controller.prototype.isInitialized = function () {
-               return this.initialized;
-       };
-
-       /**
-        * Extracts information from the changes list DOM
-        *
-        * @param {jQuery} $root Root DOM to find children from
-        * @param {boolean} [statusCode] Server response status code
-        * @return {Object} Information about changes list
-        * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
-        *   (either normally or as an error)
-        * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
-        *   'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
-        * @return {jQuery} return.fieldset Fieldset
-        */
-       mw.rcfilters.Controller.prototype._extractChangesListInfo = function ( $root, statusCode ) {
-               var info,
-                       $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
-                       areResults = !!$changesListContents.length,
-                       checkForLogout = !areResults && statusCode === 200;
-
-               // We check if user logged out on different tab/browser or the session has expired.
-               // 205 status code returned from the server, which indicates that we need to reload the page
-               // is not usable on WL page, because we get redirected to login page, which gives 200 OK
-               // status code (if everything else goes well).
-               // Bug: T177717
-               if ( checkForLogout && !!$root.find( '#wpName1' ).length ) {
-                       location.reload( false );
-                       return;
-               }
-
-               info = {
-                       changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
-                       fieldset: $root.find( 'fieldset.cloptions' ).first()
-               };
-
-               if ( !areResults ) {
-                       if ( $root.find( '.mw-changeslist-timeout' ).length ) {
-                               info.noResultsDetails = 'NO_RESULTS_TIMEOUT';
-                       } else if ( $root.find( '.mw-changeslist-notargetpage' ).length ) {
-                               info.noResultsDetails = 'NO_RESULTS_NO_TARGET_PAGE';
-                       } else if ( $root.find( '.mw-changeslist-invalidtargetpage' ).length ) {
-                               info.noResultsDetails = 'NO_RESULTS_INVALID_TARGET_PAGE';
-                       } else {
-                               info.noResultsDetails = 'NO_RESULTS_NORMAL';
-                       }
-               }
-
-               return info;
-       };
-
-       /**
-        * Create filter data from a number, for the filters that are numerical value
-        *
-        * @param {number} num Number
-        * @param {number} numForDisplay Number for the label
-        * @return {Object} Filter data
-        */
-       mw.rcfilters.Controller.prototype._createFilterDataFromNumber = function ( num, numForDisplay ) {
-               return {
-                       name: String( num ),
-                       label: mw.language.convertNumber( numForDisplay )
-               };
-       };
-
-       /**
-        * Add an arbitrary values to groups that allow arbitrary values
-        *
-        * @param {Object} groupData Group data
-        * @param {string|string[]} arbitraryValues An array of arbitrary values to add to the group
-        */
-       mw.rcfilters.Controller.prototype.addNumberValuesToGroup = function ( groupData, arbitraryValues ) {
-               var controller = this,
-                       normalizeWithinRange = function ( range, val ) {
-                               if ( val < range.min ) {
-                                       return range.min; // Min
-                               } else if ( val >= range.max ) {
-                                       return range.max; // Max
-                               }
-                               return val;
-                       };
-
-               arbitraryValues = Array.isArray( arbitraryValues ) ? arbitraryValues : [ arbitraryValues ];
-
-               // Normalize the arbitrary values and the default value for a range
-               if ( groupData.range ) {
-                       arbitraryValues = arbitraryValues.map( function ( val ) {
-                               return normalizeWithinRange( groupData.range, val );
-                       } );
-
-                       // Normalize the default, since that's user defined
-                       if ( groupData.default !== undefined ) {
-                               groupData.default = String( normalizeWithinRange( groupData.range, groupData.default ) );
-                       }
-               }
-
-               // This is only true for single_option group
-               // We assume these are the only groups that will allow for
-               // arbitrary, since it doesn't make any sense for the other
-               // groups.
-               arbitraryValues.forEach( function ( val ) {
-                       if (
-                               // If the group allows for arbitrary data
-                               groupData.allowArbitrary &&
-                               // and it is single_option (or string_options, but we
-                               // don't have cases of those yet, nor do we plan to)
-                               groupData.type === 'single_option' &&
-                               // and, if there is a validate method and it passes on
-                               // the data
-                               ( !groupData.validate || groupData.validate( val ) ) &&
-                               // but if that value isn't already in the definition
-                               groupData.filters
-                                       .map( function ( filterData ) {
-                                               return String( filterData.name );
-                                       } )
-                                       .indexOf( String( val ) ) === -1
-                       ) {
-                               // Add the filter information
-                               groupData.filters.push( controller._createFilterDataFromNumber(
-                                       val,
-                                       groupData.numToLabelFunc ?
-                                               groupData.numToLabelFunc( val ) :
-                                               val
-                               ) );
-
-                               // If there's a sort function set up, re-sort the values
-                               if ( groupData.sortFunc ) {
-                                       groupData.filters.sort( groupData.sortFunc );
-                               }
-                       }
-               } );
-       };
-
-       /**
-        * Reset to default filters
-        */
-       mw.rcfilters.Controller.prototype.resetToDefaults = function () {
-               var params = this._getDefaultParams();
-               if ( this.applyParamChange( params ) ) {
-                       // Only update the changes list if there was a change to actual filters
-                       this.updateChangesList();
-               } else {
-                       this.uriProcessor.updateURL( params );
-               }
-       };
-
-       /**
-        * Check whether the default values of the filters are all false.
-        *
-        * @return {boolean} Defaults are all false
-        */
-       mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
-               return $.isEmptyObject( this._getDefaultParams() );
-       };
-
-       /**
-        * Empty all selected filters
-        */
-       mw.rcfilters.Controller.prototype.emptyFilters = function () {
-               var highlightedFilterNames = this.filtersModel.getHighlightedItems()
-                       .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
-
-               if ( this.applyParamChange( {} ) ) {
-                       // Only update the changes list if there was a change to actual filters
-                       this.updateChangesList();
-               } else {
-                       this.uriProcessor.updateURL();
-               }
-
-               if ( highlightedFilterNames ) {
-                       this._trackHighlight( 'clearAll', highlightedFilterNames );
-               }
-       };
-
-       /**
-        * Update the selected state of a filter
-        *
-        * @param {string} filterName Filter name
-        * @param {boolean} [isSelected] Filter selected state
-        */
-       mw.rcfilters.Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) {
-               var filterItem = this.filtersModel.getItemByName( filterName );
-
-               if ( !filterItem ) {
-                       // If no filter was found, break
-                       return;
-               }
-
-               isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected;
-
-               if ( filterItem.isSelected() !== isSelected ) {
-                       this.filtersModel.toggleFilterSelected( filterName, isSelected );
-
-                       this.updateChangesList();
-
-                       // Check filter interactions
-                       this.filtersModel.reassessFilterInteractions( filterItem );
-               }
-       };
-
-       /**
-        * Clear both highlight and selection of a filter
-        *
-        * @param {string} filterName Name of the filter item
-        */
-       mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
-               var filterItem = this.filtersModel.getItemByName( filterName ),
-                       isHighlighted = filterItem.isHighlighted(),
-                       isSelected = filterItem.isSelected();
-
-               if ( isSelected || isHighlighted ) {
-                       this.filtersModel.clearHighlightColor( filterName );
-                       this.filtersModel.toggleFilterSelected( filterName, false );
-
-                       if ( isSelected ) {
-                               // Only update the changes list if the filter changed
-                               // its selection state. If it only changed its highlight
-                               // then don't reload
-                               this.updateChangesList();
-                       }
-
-                       this.filtersModel.reassessFilterInteractions( filterItem );
-
-                       // Log filter grouping
-                       this.trackFilterGroupings( 'removefilter' );
-               }
-
-               if ( isHighlighted ) {
-                       this._trackHighlight( 'clear', filterName );
-               }
-       };
-
-       /**
-        * Toggle the highlight feature on and off
-        */
-       mw.rcfilters.Controller.prototype.toggleHighlight = function () {
-               this.filtersModel.toggleHighlight();
-               this.uriProcessor.updateURL();
-
-               if ( this.filtersModel.isHighlightEnabled() ) {
-                       mw.hook( 'RcFilters.highlight.enable' ).fire();
-               }
-       };
-
-       /**
-        * Toggle the namespaces inverted feature on and off
-        */
-       mw.rcfilters.Controller.prototype.toggleInvertedNamespaces = function () {
-               this.filtersModel.toggleInvertedNamespaces();
-               if (
-                       this.filtersModel.getFiltersByView( 'namespaces' ).filter(
-                               function ( filterItem ) { return filterItem.isSelected(); }
-                       ).length
-               ) {
-                       // Only re-fetch results if there are namespace items that are actually selected
-                       this.updateChangesList();
-               } else {
-                       this.uriProcessor.updateURL();
-               }
-       };
-
-       /**
-        * Set the value of the 'showlinkedto' parameter
-        * @param {boolean} value
-        */
-       mw.rcfilters.Controller.prototype.setShowLinkedTo = function ( value ) {
-               var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' ),
-                       showLinkedToItem = this.filtersModel.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' );
-
-               this.filtersModel.toggleFilterSelected( showLinkedToItem.getName(), value );
-               this.uriProcessor.updateURL();
-               // reload the results only when target is set
-               if ( targetItem.getValue() ) {
-                       this.updateChangesList();
-               }
-       };
-
-       /**
-        * Set the target page
-        * @param {string} page
-        */
-       mw.rcfilters.Controller.prototype.setTargetPage = function ( page ) {
-               var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' );
-               targetItem.setValue( page );
-               this.uriProcessor.updateURL();
-               this.updateChangesList();
-       };
-
-       /**
-        * Set the highlight color for a filter item
-        *
-        * @param {string} filterName Name of the filter item
-        * @param {string} color Selected color
-        */
-       mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
-               this.filtersModel.setHighlightColor( filterName, color );
-               this.uriProcessor.updateURL();
-               this._trackHighlight( 'set', { name: filterName, color: color } );
-       };
-
-       /**
-        * Clear highlight for a filter item
-        *
-        * @param {string} filterName Name of the filter item
-        */
-       mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
-               this.filtersModel.clearHighlightColor( filterName );
-               this.uriProcessor.updateURL();
-               this._trackHighlight( 'clear', filterName );
-       };
-
-       /**
-        * Enable or disable live updates.
-        * @param {boolean} enable True to enable, false to disable
-        */
-       mw.rcfilters.Controller.prototype.toggleLiveUpdate = function ( enable ) {
-               this.changesListModel.toggleLiveUpdate( enable );
-               if ( this.changesListModel.getLiveUpdate() && this.changesListModel.getNewChangesExist() ) {
-                       this.updateChangesList( null, this.LIVE_UPDATE );
-               }
-       };
-
-       /**
-        * Set a timeout for the next live update.
-        * @private
-        */
-       mw.rcfilters.Controller.prototype._scheduleLiveUpdate = function () {
-               setTimeout( this._doLiveUpdate.bind( this ), this.pollingRate * 1000 );
-       };
-
-       /**
-        * Perform a live update.
-        * @private
-        */
-       mw.rcfilters.Controller.prototype._doLiveUpdate = function () {
-               if ( !this._shouldCheckForNewChanges() ) {
-                       // skip this turn and check back later
-                       this._scheduleLiveUpdate();
-                       return;
-               }
-
-               this._checkForNewChanges()
-                       .then( function ( statusCode ) {
-                               // no result is 204 with the 'peek' param
-                               // logged out is 205
-                               var newChanges = statusCode === 200;
-
-                               if ( !this._shouldCheckForNewChanges() ) {
-                                       // by the time the response is received,
-                                       // it may not be appropriate anymore
-                                       return;
-                               }
-
-                               // 205 is the status code returned from server when user's logged in/out
-                               // status is not matching while fetching live update changes.
-                               // This works only on Recent Changes page. For WL, look _extractChangesListInfo.
-                               // Bug: T177717
-                               if ( statusCode === 205 ) {
-                                       location.reload( false );
-                                       return;
-                               }
-
-                               if ( newChanges ) {
-                                       if ( this.changesListModel.getLiveUpdate() ) {
-                                               return this.updateChangesList( null, this.LIVE_UPDATE );
-                                       } else {
-                                               this.changesListModel.setNewChangesExist( true );
-                                       }
-                               }
-                       }.bind( this ) )
-                       .always( this._scheduleLiveUpdate.bind( this ) );
-       };
-
-       /**
-        * @return {boolean} It's appropriate to check for new changes now
-        * @private
-        */
-       mw.rcfilters.Controller.prototype._shouldCheckForNewChanges = function () {
-               return !document.hidden &&
-                       !this.filtersModel.hasConflict() &&
-                       !this.changesListModel.getNewChangesExist() &&
-                       !this.updatingChangesList &&
-                       this.changesListModel.getNextFrom();
-       };
-
-       /**
-        * Check if new changes, newer than those currently shown, are available
-        *
-        * @return {jQuery.Promise} Promise object that resolves with a bool
-        *   specifying if there are new changes or not
-        *
-        * @private
-        */
-       mw.rcfilters.Controller.prototype._checkForNewChanges = function () {
-               var params = {
-                       limit: 1,
-                       peek: 1, // bypasses ChangesList specific UI
-                       from: this.changesListModel.getNextFrom(),
-                       isAnon: mw.user.isAnon()
-               };
-               return this._queryChangesList( 'liveUpdate', params ).then(
-                       function ( data ) {
-                               return data.status;
-                       }
-               );
-       };
-
-       /**
-        * Show the new changes
-        *
-        * @return {jQuery.Promise} Promise object that resolves after
-        * fetching and showing the new changes
-        */
-       mw.rcfilters.Controller.prototype.showNewChanges = function () {
-               return this.updateChangesList( null, this.SHOW_NEW_CHANGES );
-       };
-
-       /**
-        * Save the current model state as a saved query
-        *
-        * @param {string} [label] Label of the saved query
-        * @param {boolean} [setAsDefault=false] This query should be set as the default
-        */
-       mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
-               // Add item
-               this.savedQueriesModel.addNewQuery(
-                       label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
-                       this.filtersModel.getCurrentParameterState( true ),
-                       setAsDefault
-               );
-
-               // Save item
-               this._saveSavedQueries();
-       };
-
-       /**
-        * Remove a saved query
-        *
-        * @param {string} queryID Query id
-        */
-       mw.rcfilters.Controller.prototype.removeSavedQuery = function ( queryID ) {
-               this.savedQueriesModel.removeQuery( queryID );
-
-               this._saveSavedQueries();
-       };
-
-       /**
-        * Rename a saved query
-        *
-        * @param {string} queryID Query id
-        * @param {string} newLabel New label for the query
-        */
-       mw.rcfilters.Controller.prototype.renameSavedQuery = function ( queryID, newLabel ) {
-               var queryItem = this.savedQueriesModel.getItemByID( queryID );
-
-               if ( queryItem ) {
-                       queryItem.updateLabel( newLabel );
-               }
-               this._saveSavedQueries();
-       };
-
-       /**
-        * Set a saved query as default
-        *
-        * @param {string} queryID Query Id. If null is given, default
-        *  query is reset.
-        */
-       mw.rcfilters.Controller.prototype.setDefaultSavedQuery = function ( queryID ) {
-               this.savedQueriesModel.setDefault( queryID );
-               this._saveSavedQueries();
-       };
-
-       /**
-        * Load a saved query
-        *
-        * @param {string} queryID Query id
-        */
-       mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
-               var currentMatchingQuery,
-                       params = this.savedQueriesModel.getItemParams( queryID );
-
-               currentMatchingQuery = this.findQueryMatchingCurrentState();
-
-               if (
-                       currentMatchingQuery &&
-                       currentMatchingQuery.getID() === queryID
-               ) {
-                       // If the query we want to load is the one that is already
-                       // loaded, don't reload it
-                       return;
-               }
-
-               if ( this.applyParamChange( params ) ) {
-                       // Update changes list only if there was a difference in filter selection
-                       this.updateChangesList();
-               } else {
-                       this.uriProcessor.updateURL( params );
-               }
-
-               // Log filter grouping
-               this.trackFilterGroupings( 'savedfilters' );
-       };
-
-       /**
-        * Check whether the current filter and highlight state exists
-        * in the saved queries model.
-        *
-        * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
-        */
-       mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
-               return this.savedQueriesModel.findMatchingQuery(
-                       this.filtersModel.getCurrentParameterState( true )
-               );
-       };
-
-       /**
-        * Save the current state of the saved queries model with all
-        * query item representation in the user settings.
-        */
-       mw.rcfilters.Controller.prototype._saveSavedQueries = function () {
-               var stringified, oldPrefValue,
-                       backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
-                       state = this.savedQueriesModel.getState();
-
-               // Stringify state
-               stringified = JSON.stringify( state );
-
-               if ( byteLength( stringified ) > 65535 ) {
-                       // Sanity check, since the preference can only hold that.
-                       return;
-               }
-
-               if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
-                       // The queries were converted from the previous version
-                       // Keep the old string in the [prefname]-versionbackup
-                       oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
-
-                       // Save the old preference in the backup preference
-                       new mw.Api().saveOption( backupPrefName, oldPrefValue );
-                       // Update the preference for this session
-                       mw.user.options.set( backupPrefName, oldPrefValue );
-               }
-
-               // Save the preference
-               new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
-               // Update the preference for this session
-               mw.user.options.set( this.savedQueriesPreferenceName, stringified );
-
-               // Tag as already saved so we don't do this again
-               this.wereSavedQueriesSaved = true;
-       };
-
-       /**
-        * Update sticky preferences with current model state
-        */
-       mw.rcfilters.Controller.prototype.updateStickyPreferences = function () {
-               // Update default sticky values with selected, whether they came from
-               // the initial defaults or from the URL value that is being normalized
-               this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).findSelectedItems()[ 0 ].getParamName() );
-               this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).findSelectedItems()[ 0 ].getParamName() );
-
-               // TODO: Make these automatic by having the model go over sticky
-               // items and update their default values automatically
-       };
-
-       /**
-        * Update the limit default value
-        *
-        * @param {number} newValue New value
-        */
-       mw.rcfilters.Controller.prototype.updateLimitDefault = function ( newValue ) {
-               this.updateNumericPreference( this.limitPreferenceName, newValue );
-       };
-
-       /**
-        * Update the days default value
-        *
-        * @param {number} newValue New value
-        */
-       mw.rcfilters.Controller.prototype.updateDaysDefault = function ( newValue ) {
-               this.updateNumericPreference( this.daysPreferenceName, newValue );
-       };
-
-       /**
-        * Update the group by page default value
-        *
-        * @param {boolean} newValue New value
-        */
-       mw.rcfilters.Controller.prototype.updateGroupByPageDefault = function ( newValue ) {
-               this.updateNumericPreference( 'usenewrc', Number( newValue ) );
-       };
-
-       /**
-        * Update the collapsed state value
-        *
-        * @param {boolean} isCollapsed Filter area is collapsed
-        */
-       mw.rcfilters.Controller.prototype.updateCollapsedState = function ( isCollapsed ) {
-               this.updateNumericPreference( this.collapsedPreferenceName, Number( isCollapsed ) );
-       };
-
-       /**
-        * Update a numeric preference with a new value
-        *
-        * @param {string} prefName Preference name
-        * @param {number|string} newValue New value
-        */
-       mw.rcfilters.Controller.prototype.updateNumericPreference = function ( prefName, newValue ) {
-               if ( !$.isNumeric( newValue ) ) {
-                       return;
-               }
-
-               newValue = Number( newValue );
-
-               if ( mw.user.options.get( prefName ) !== newValue ) {
-                       // Save the preference
-                       new mw.Api().saveOption( prefName, newValue );
-                       // Update the preference for this session
-                       mw.user.options.set( prefName, newValue );
-               }
-       };
-
-       /**
-        * Synchronize the URL with the current state of the filters
-        * without adding an history entry.
-        */
-       mw.rcfilters.Controller.prototype.replaceUrl = function () {
-               this.uriProcessor.updateURL();
-       };
-
-       /**
-        * Update filter state (selection and highlighting) based
-        * on current URL values.
-        *
-        * @param {boolean} [fetchChangesList=true] Fetch new results into the changes
-        *  list based on the updated model.
-        */
-       mw.rcfilters.Controller.prototype.updateStateFromUrl = function ( fetchChangesList ) {
-               fetchChangesList = fetchChangesList === undefined ? true : !!fetchChangesList;
-
-               this.uriProcessor.updateModelBasedOnQuery();
-
-               // Update the sticky preferences, in case we received a value
-               // from the URL
-               this.updateStickyPreferences();
-
-               // Only update and fetch new results if it is requested
-               if ( fetchChangesList ) {
-                       this.updateChangesList();
-               }
-       };
-
-       /**
-        * Update the list of changes and notify the model
-        *
-        * @param {Object} [params] Extra parameters to add to the API call
-        * @param {string} [updateMode='filterChange'] One of 'filterChange', 'liveUpdate', 'showNewChanges', 'markSeen'
-        * @return {jQuery.Promise} Promise that is resolved when the update is complete
-        */
-       mw.rcfilters.Controller.prototype.updateChangesList = function ( params, updateMode ) {
-               updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode;
-
-               if ( updateMode === this.FILTER_CHANGE ) {
-                       this.uriProcessor.updateURL( params );
-               }
-               if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) {
-                       this.changesListModel.invalidate();
-               }
-               this.changesListModel.setNewChangesExist( false );
-               this.updatingChangesList = true;
-               return this._fetchChangesList()
-                       .then(
-                               // Success
-                               function ( pieces ) {
-                                       var $changesListContent = pieces.changes,
-                                               $fieldset = pieces.fieldset;
-                                       this.changesListModel.update(
-                                               $changesListContent,
-                                               $fieldset,
-                                               pieces.noResultsDetails,
-                                               false,
-                                               // separator between old and new changes
-                                               updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
-                                       );
-                               }.bind( this )
-                               // Do nothing for failure
-                       )
-                       .always( function () {
-                               this.updatingChangesList = false;
-                       }.bind( this ) );
-       };
-
-       /**
-        * Get an object representing the default parameter state, whether
-        * it is from the model defaults or from the saved queries.
-        *
-        * @return {Object} Default parameters
-        */
-       mw.rcfilters.Controller.prototype._getDefaultParams = function () {
-               if ( this.savedQueriesModel.getDefault() ) {
-                       return this.savedQueriesModel.getDefaultParams();
-               } else {
-                       return this.filtersModel.getDefaultParams();
-               }
-       };
-
-       /**
-        * Query the list of changes from the server for the current filters
-        *
-        * @param {string} counterId Id for this request. To allow concurrent requests
-        *  not to invalidate each other.
-        * @param {Object} [params={}] Parameters to add to the query
-        *
-        * @return {jQuery.Promise} Promise object resolved with { content, status }
-        */
-       mw.rcfilters.Controller.prototype._queryChangesList = function ( counterId, params ) {
-               var uri = this.uriProcessor.getUpdatedUri(),
-                       stickyParams = this.filtersModel.getStickyParamsValues(),
-                       requestId,
-                       latestRequest;
-
-               params = params || {};
-               params.action = 'render'; // bypasses MW chrome
-
-               uri.extend( params );
-
-               this.requestCounter[ counterId ] = this.requestCounter[ counterId ] || 0;
-               requestId = ++this.requestCounter[ counterId ];
-               latestRequest = function () {
-                       return requestId === this.requestCounter[ counterId ];
-               }.bind( this );
-
-               // Sticky parameters override the URL params
-               // this is to make sure that whether we represent
-               // the sticky params in the URL or not (they may
-               // be normalized out) the sticky parameters are
-               // always being sent to the server with their
-               // current/default values
-               uri.extend( stickyParams );
-
-               return $.ajax( uri.toString(), { contentType: 'html' } )
-                       .then(
-                               function ( content, message, jqXHR ) {
-                                       if ( !latestRequest() ) {
-                                               return $.Deferred().reject();
-                                       }
-                                       return {
-                                               content: content,
-                                               status: jqXHR.status
-                                       };
-                               },
-                               // RC returns 404 when there is no results
-                               function ( jqXHR ) {
-                                       if ( latestRequest() ) {
-                                               return $.Deferred().resolve(
-                                                       {
-                                                               content: jqXHR.responseText,
-                                                               status: jqXHR.status
-                                                       }
-                                               ).promise();
-                                       }
-                               }
-                       );
-       };
-
-       /**
-        * Fetch the list of changes from the server for the current filters
-        *
-        * @return {jQuery.Promise} Promise object that will resolve with the changes list
-        *  and the fieldset.
-        */
-       mw.rcfilters.Controller.prototype._fetchChangesList = function () {
-               return this._queryChangesList( 'updateChangesList' )
-                       .then(
-                               function ( data ) {
-                                       var $parsed;
-
-                                       // Status code 0 is not HTTP status code,
-                                       // but is valid value of XMLHttpRequest status.
-                                       // It is used for variety of network errors, for example
-                                       // when an AJAX call was cancelled before getting the response
-                                       if ( data && data.status === 0 ) {
-                                               return {
-                                                       changes: 'NO_RESULTS',
-                                                       // We need empty result set, to avoid exceptions because of undefined value
-                                                       fieldset: $( [] ),
-                                                       noResultsDetails: 'NO_RESULTS_NETWORK_ERROR'
-                                               };
-                                       }
-
-                                       $parsed = $( '<div>' ).append( $( $.parseHTML(
-                                               data ? data.content : ''
-                                       ) ) );
-
-                                       return this._extractChangesListInfo( $parsed, data.status );
-                               }.bind( this )
-                       );
-       };
-
-       /**
-        * Track usage of highlight feature
-        *
-        * @param {string} action
-        * @param {Array|Object|string} filters
-        */
-       mw.rcfilters.Controller.prototype._trackHighlight = function ( action, filters ) {
-               filters = typeof filters === 'string' ? { name: filters } : filters;
-               filters = !Array.isArray( filters ) ? [ filters ] : filters;
-               mw.track(
-                       'event.ChangesListHighlights',
-                       {
-                               action: action,
-                               filters: filters,
-                               userId: mw.user.getId()
-                       }
-               );
-       };
-
-       /**
-        * Track filter grouping usage
-        *
-        * @param {string} action Action taken
-        */
-       mw.rcfilters.Controller.prototype.trackFilterGroupings = function ( action ) {
-               var controller = this,
-                       rightNow = new Date().getTime(),
-                       randomIdentifier = String( mw.user.sessionId() ) + String( rightNow ) + String( Math.random() ),
-                       // Get all current filters
-                       filters = this.filtersModel.findSelectedItems().map( function ( item ) {
-                               return item.getName();
-                       } );
-
-               action = action || 'filtermenu';
-
-               // Check if these filters were the ones we just logged previously
-               // (Don't log the same grouping twice, in case the user opens/closes)
-               // the menu without action, or with the same result
-               if (
-                       // Only log if the two arrays are different in size
-                       filters.length !== this.prevLoggedItems.length ||
-                       // Or if any filters are not the same as the cached filters
-                       filters.some( function ( filterName ) {
-                               return controller.prevLoggedItems.indexOf( filterName ) === -1;
-                       } ) ||
-                       // Or if any cached filters are not the same as given filters
-                       this.prevLoggedItems.some( function ( filterName ) {
-                               return filters.indexOf( filterName ) === -1;
-                       } )
-               ) {
-                       filters.forEach( function ( filterName ) {
-                               mw.track(
-                                       'event.ChangesListFilterGrouping',
-                                       {
-                                               action: action,
-                                               groupIdentifier: randomIdentifier,
-                                               filter: filterName,
-                                               userId: mw.user.getId()
-                                       }
-                               );
-                       } );
-
-                       // Cache the filter names
-                       this.prevLoggedItems = filters;
-               }
-       };
-
-       /**
-        * Apply a change of parameters to the model state, and check whether
-        * the new state is different than the old state.
-        *
-        * @param  {Object} newParamState New parameter state to apply
-        * @return {boolean} New applied model state is different than the previous state
-        */
-       mw.rcfilters.Controller.prototype.applyParamChange = function ( newParamState ) {
-               var after,
-                       before = this.filtersModel.getSelectedState();
-
-               this.filtersModel.updateStateFromParams( newParamState );
-
-               after = this.filtersModel.getSelectedState();
-
-               return !OO.compare( before, after );
-       };
-
-       /**
-        * Mark all changes as seen on Watchlist
-        */
-       mw.rcfilters.Controller.prototype.markAllChangesAsSeen = function () {
-               var api = new mw.Api();
-               api.postWithToken( 'csrf', {
-                       formatversion: 2,
-                       action: 'setnotificationtimestamp',
-                       entirewatchlist: true
-               } ).then( function () {
-                       this.updateChangesList( null, 'markSeen' );
-               }.bind( this ) );
-       };
-
-       /**
-        * Set the current search for the system.
-        *
-        * @param {string} searchQuery Search query, including triggers
-        */
-       mw.rcfilters.Controller.prototype.setSearch = function ( searchQuery ) {
-               this.filtersModel.setSearch( searchQuery );
-       };
-
-       /**
-        * Switch the view by changing the search query trigger
-        * without changing the search term
-        *
-        * @param  {string} view View to change to
-        */
-       mw.rcfilters.Controller.prototype.switchView = function ( view ) {
-               this.setSearch(
-                       this.filtersModel.getViewTrigger( view ) +
-                       this.filtersModel.removeViewTriggers( this.filtersModel.getSearch() )
-               );
-       };
-
-       /**
-        * Reset the search for a specific view. This means we null the search query
-        * and replace it with the relevant trigger for the requested view
-        *
-        * @param  {string} [view='default'] View to change to
-        */
-       mw.rcfilters.Controller.prototype.resetSearchForView = function ( view ) {
-               view = view || 'default';
-
-               this.setSearch(
-                       this.filtersModel.getViewTrigger( view )
-               );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js
deleted file mode 100644 (file)
index 6231f28..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-( function () {
-       /**
-        * Supported highlight colors.
-        * Warning: These are also hardcoded in "styles/mw.rcfilters.variables.less"
-        *
-        * @member mw.rcfilters
-        * @property {string[]}
-        */
-       mw.rcfilters.HighlightColors = [ 'c1', 'c2', 'c3', 'c4', 'c5' ];
-}() );
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
deleted file mode 100644 (file)
index 5344af4..0000000
+++ /dev/null
@@ -1,294 +0,0 @@
-( function () {
-       /* eslint no-underscore-dangle: "off" */
-       /**
-        * URI Processor for RCFilters
-        *
-        * @class
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model
-        * @param {Object} [config] Configuration object
-        * @cfg {boolean} [normalizeTarget] Dictates whether or not to go through the
-        *  title normalization to separate title subpage/parts into the target= url
-        *  parameter
-        */
-       mw.rcfilters.UriProcessor = function MwRcfiltersController( filtersModel, config ) {
-               config = config || {};
-               this.filtersModel = filtersModel;
-
-               this.normalizeTarget = !!config.normalizeTarget;
-       };
-
-       /* Initialization */
-       OO.initClass( mw.rcfilters.UriProcessor );
-
-       /* Static methods */
-
-       /**
-        * Replace the url history through replaceState
-        *
-        * @param {mw.Uri} newUri New URI to replace
-        */
-       mw.rcfilters.UriProcessor.static.replaceState = function ( newUri ) {
-               window.history.replaceState(
-                       { tag: 'rcfilters' },
-                       document.title,
-                       newUri.toString()
-               );
-       };
-
-       /**
-        * Push the url to history through pushState
-        *
-        * @param {mw.Uri} newUri New URI to push
-        */
-       mw.rcfilters.UriProcessor.static.pushState = function ( newUri ) {
-               window.history.pushState(
-                       { tag: 'rcfilters' },
-                       document.title,
-                       newUri.toString()
-               );
-       };
-
-       /* Methods */
-
-       /**
-        * Get the version that this URL query is tagged with.
-        *
-        * @param {Object} [uriQuery] URI query
-        * @return {number} URL version
-        */
-       mw.rcfilters.UriProcessor.prototype.getVersion = function ( uriQuery ) {
-               uriQuery = uriQuery || new mw.Uri().query;
-
-               return Number( uriQuery.urlversion || 1 );
-       };
-
-       /**
-        * Get an updated mw.Uri object based on the model state
-        *
-        * @param {mw.Uri} [uri] An external URI to build the new uri
-        *  with. This is mainly for tests, to be able to supply external query
-        *  parameters and make sure they are retained.
-        * @return {mw.Uri} Updated Uri
-        */
-       mw.rcfilters.UriProcessor.prototype.getUpdatedUri = function ( uri ) {
-               var normalizedUri = this._normalizeTargetInUri( uri || new mw.Uri() ),
-                       unrecognizedParams = this.getUnrecognizedParams( normalizedUri.query );
-
-               normalizedUri.query = this.filtersModel.getMinimizedParamRepresentation(
-                       $.extend(
-                               true,
-                               {},
-                               normalizedUri.query,
-                               // The representation must be expanded so it can
-                               // override the uri query params but we then output
-                               // a minimized version for the entire URI representation
-                               // for the method
-                               this.filtersModel.getExpandedParamRepresentation()
-                       )
-               );
-
-               // Reapply unrecognized params and url version
-               normalizedUri.query = $.extend(
-                       true,
-                       {},
-                       normalizedUri.query,
-                       unrecognizedParams,
-                       { urlversion: '2' }
-               );
-
-               return normalizedUri;
-       };
-
-       /**
-        * Move the subpage to the target parameter
-        *
-        * @param {mw.Uri} uri
-        * @return {mw.Uri}
-        * @private
-        */
-       mw.rcfilters.UriProcessor.prototype._normalizeTargetInUri = function ( uri ) {
-               var parts,
-                       // matches [/wiki/]SpecialNS:RCL/[Namespace:]Title/Subpage/Subsubpage/etc
-                       re = /^((?:\/.+?\/)?.*?:.*?)\/(.*)$/;
-
-               if ( !this.normalizeTarget ) {
-                       return uri;
-               }
-
-               // target in title param
-               if ( uri.query.title ) {
-                       parts = uri.query.title.match( re );
-                       if ( parts ) {
-                               uri.query.title = parts[ 1 ];
-                               uri.query.target = parts[ 2 ];
-                       }
-               }
-
-               // target in path
-               parts = mw.Uri.decode( uri.path ).match( re );
-               if ( parts ) {
-                       uri.path = parts[ 1 ];
-                       uri.query.target = parts[ 2 ];
-               }
-
-               return uri;
-       };
-
-       /**
-        * Get an object representing given parameters that are unrecognized by the model
-        *
-        * @param  {Object} params Full params object
-        * @return {Object} Unrecognized params
-        */
-       mw.rcfilters.UriProcessor.prototype.getUnrecognizedParams = function ( params ) {
-               // Start with full representation
-               var givenParamNames = Object.keys( params ),
-                       unrecognizedParams = $.extend( true, {}, params );
-
-               // Extract unrecognized parameters
-               Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) {
-                       // Remove recognized params
-                       if ( givenParamNames.indexOf( paramName ) > -1 ) {
-                               delete unrecognizedParams[ paramName ];
-                       }
-               } );
-
-               return unrecognizedParams;
-       };
-
-       /**
-        * Update the URL of the page to reflect current filters
-        *
-        * This should not be called directly from outside the controller.
-        * If an action requires changing the URL, it should either use the
-        * highlighting actions below, or call #updateChangesList which does
-        * the uri corrections already.
-        *
-        * @param {Object} [params] Extra parameters to add to the API call
-        */
-       mw.rcfilters.UriProcessor.prototype.updateURL = function ( params ) {
-               var currentUri = new mw.Uri(),
-                       updatedUri = this.getUpdatedUri();
-
-               updatedUri.extend( params || {} );
-
-               if (
-                       this.getVersion( currentUri.query ) !== 2 ||
-                       this.isNewState( currentUri.query, updatedUri.query )
-               ) {
-                       this.constructor.static.replaceState( updatedUri );
-               }
-       };
-
-       /**
-        * Update the filters model based on the URI query
-        * This happens on initialization, and from this moment on,
-        * we consider the system synchronized, and the model serves
-        * as the source of truth for the URL.
-        *
-        * This methods should only be called once on initialization.
-        * After initialization, the model updates the URL, not the
-        * other way around.
-        *
-        * @param {Object} [uriQuery] URI query
-        */
-       mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) {
-               uriQuery = uriQuery || this._normalizeTargetInUri( new mw.Uri() ).query;
-               this.filtersModel.updateStateFromParams(
-                       this._getNormalizedQueryParams( uriQuery )
-               );
-       };
-
-       /**
-        * Compare two URI queries to decide whether they are different
-        * enough to represent a new state.
-        *
-        * @param {Object} currentUriQuery Current Uri query
-        * @param {Object} updatedUriQuery Updated Uri query
-        * @return {boolean} This is a new state
-        */
-       mw.rcfilters.UriProcessor.prototype.isNewState = function ( currentUriQuery, updatedUriQuery ) {
-               var currentParamState, updatedParamState,
-                       notEquivalent = function ( obj1, obj2 ) {
-                               var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) );
-                               return keys.some( function ( key ) {
-                                       return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq
-                               } );
-                       };
-
-               // Compare states instead of parameters
-               // This will allow us to always have a proper check of whether
-               // the requested new url is one to change or not, regardless of
-               // actual parameter visibility/representation in the URL
-               currentParamState = $.extend(
-                       true,
-                       {},
-                       this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ),
-                       this.getUnrecognizedParams( currentUriQuery )
-               );
-               updatedParamState = $.extend(
-                       true,
-                       {},
-                       this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ),
-                       this.getUnrecognizedParams( updatedUriQuery )
-               );
-
-               return notEquivalent( currentParamState, updatedParamState );
-       };
-
-       /**
-        * Check whether the given query has parameters that are
-        * recognized as parameters we should load the system with
-        *
-        * @param {mw.Uri} [uriQuery] Given URI query
-        * @return {boolean} Query contains valid recognized parameters
-        */
-       mw.rcfilters.UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) {
-               var anyValidInUrl,
-                       validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() );
-
-               uriQuery = uriQuery || new mw.Uri().query;
-
-               anyValidInUrl = Object.keys( uriQuery ).some( function ( parameter ) {
-                       return validParameterNames.indexOf( parameter ) > -1;
-               } );
-
-               // URL version 2 is allowed to be empty or within nonrecognized params
-               return anyValidInUrl || this.getVersion( uriQuery ) === 2;
-       };
-
-       /**
-        * Get the adjusted URI params based on the url version
-        * If the urlversion is not 2, the parameters are merged with
-        * the model's defaults.
-        * Always merge in the hidden parameter defaults.
-        *
-        * @private
-        * @param {Object} uriQuery Current URI query
-        * @return {Object} Normalized parameters
-        */
-       mw.rcfilters.UriProcessor.prototype._getNormalizedQueryParams = function ( uriQuery ) {
-               // Check whether we are dealing with urlversion=2
-               // If we are, we do not merge the initial request with
-               // defaults. Not having urlversion=2 means we need to
-               // reproduce the server-side request and merge the
-               // requested parameters (or starting state) with the
-               // wiki default.
-               // Any subsequent change of the URL through the RCFilters
-               // system will receive 'urlversion=2'
-               var base = this.getVersion( uriQuery ) === 2 ?
-                       {} :
-                       this.filtersModel.getDefaultParams();
-
-               return $.extend(
-                       true,
-                       {},
-                       this.filtersModel.getMinimizedParamRepresentation(
-                               $.extend( true, {}, base, uriQuery )
-                       ),
-                       { urlversion: '2' }
-               );
-       };
-}() );
index f866aa4..a69dc55 100644 (file)
  * JavaScript for Special:RecentChanges
  */
 ( function () {
-       var rcfilters = {
-               /**
-                * @member mw.rcfilters
-                * @private
-                */
-               init: function () {
-                       var $topSection,
-                               mainWrapperWidget,
-                               conditionalViews = {},
-                               $initialFieldset = $( 'fieldset.cloptions' ),
-                               savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
-                               daysPreferenceName = mw.config.get( 'wgStructuredChangeFiltersDaysPreferenceName' ),
-                               limitPreferenceName = mw.config.get( 'wgStructuredChangeFiltersLimitPreferenceName' ),
-                               activeFiltersCollapsedName = mw.config.get( 'wgStructuredChangeFiltersCollapsedPreferenceName' ),
-                               initialCollapsedState = mw.config.get( 'wgStructuredChangeFiltersCollapsedState' ),
-                               filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
-                               changesListModel = new mw.rcfilters.dm.ChangesListViewModel( $initialFieldset ),
-                               savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
-                               specialPage = mw.config.get( 'wgCanonicalSpecialPageName' ),
-                               controller = new mw.rcfilters.Controller(
-                                       filtersModel, changesListModel, savedQueriesModel,
-                                       {
-                                               savedQueriesPreferenceName: savedQueriesPreferenceName,
-                                               daysPreferenceName: daysPreferenceName,
-                                               limitPreferenceName: limitPreferenceName,
-                                               collapsedPreferenceName: activeFiltersCollapsedName,
-                                               normalizeTarget: specialPage === 'Recentchangeslinked'
-                                       }
-                               );
-
-                       // TODO: The changesListWrapperWidget should be able to initialize
-                       // after the model is ready.
-
-                       if ( specialPage === 'Recentchanges' ) {
-                               $topSection = $( '.mw-recentchanges-toplinks' ).detach();
-                       } else if ( specialPage === 'Watchlist' ) {
-                               $( '#contentSub, form#mw-watchlist-resetbutton' ).remove();
-                               $topSection = $( '.watchlistDetails' ).detach().contents();
-                       } else if ( specialPage === 'Recentchangeslinked' ) {
-                               conditionalViews.recentChangesLinked = {
-                                       groups: [
-                                               {
-                                                       name: 'page',
-                                                       type: 'any_value',
-                                                       title: '',
-                                                       hidden: true,
-                                                       sticky: true,
-                                                       filters: [
-                                                               {
-                                                                       name: 'target',
-                                                                       default: ''
-                                                               }
-                                                       ]
-                                               },
-                                               {
-                                                       name: 'toOrFrom',
-                                                       type: 'boolean',
-                                                       title: '',
-                                                       hidden: true,
-                                                       sticky: true,
-                                                       filters: [
-                                                               {
-                                                                       name: 'showlinkedto',
-                                                                       default: false
-                                                               }
-                                                       ]
-                                               }
-                                       ]
-                               };
-                       }
 
-                       mainWrapperWidget = new mw.rcfilters.ui.MainWrapperWidget(
-                               controller,
-                               filtersModel,
-                               savedQueriesModel,
-                               changesListModel,
+       mw.rcfilters.HighlightColors = require( './HighlightColors.js' );
+       mw.rcfilters.ui.MainWrapperWidget = require( './ui/MainWrapperWidget.js' );
+
+       /**
+        * Get list of namespaces and remove unused ones
+        *
+        * @member mw.rcfilters
+        * @private
+        *
+        * @param {Array} unusedNamespaces Names of namespaces to remove
+        * @return {Array} Filtered array of namespaces
+        */
+       function getNamespaces( unusedNamespaces ) {
+               var i, length, name, id,
+                       namespaceIds = mw.config.get( 'wgNamespaceIds' ),
+                       namespaces = mw.config.get( 'wgFormattedNamespaces' );
+
+               for ( i = 0, length = unusedNamespaces.length; i < length; i++ ) {
+                       name = unusedNamespaces[ i ];
+                       id = namespaceIds[ name.toLowerCase() ];
+                       delete namespaces[ id ];
+               }
+
+               return namespaces;
+       }
+
+       /**
+        * @member mw.rcfilters
+        * @private
+        */
+       function init() {
+               var $topSection,
+                       mainWrapperWidget,
+                       conditionalViews = {},
+                       $initialFieldset = $( 'fieldset.cloptions' ),
+                       savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
+                       daysPreferenceName = mw.config.get( 'wgStructuredChangeFiltersDaysPreferenceName' ),
+                       limitPreferenceName = mw.config.get( 'wgStructuredChangeFiltersLimitPreferenceName' ),
+                       activeFiltersCollapsedName = mw.config.get( 'wgStructuredChangeFiltersCollapsedPreferenceName' ),
+                       initialCollapsedState = mw.config.get( 'wgStructuredChangeFiltersCollapsedState' ),
+                       filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+                       changesListModel = new mw.rcfilters.dm.ChangesListViewModel( $initialFieldset ),
+                       savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
+                       specialPage = mw.config.get( 'wgCanonicalSpecialPageName' ),
+                       controller = new mw.rcfilters.Controller(
+                               filtersModel, changesListModel, savedQueriesModel,
                                {
-                                       $wrapper: $( 'body' ),
-                                       $topSection: $topSection,
-                                       $filtersContainer: $( '.rcfilters-container' ),
-                                       $changesListContainer: $( '.mw-changeslist, .mw-changeslist-empty' ),
-                                       $formContainer: $initialFieldset,
-                                       collapsed: initialCollapsedState
+                                       savedQueriesPreferenceName: savedQueriesPreferenceName,
+                                       daysPreferenceName: daysPreferenceName,
+                                       limitPreferenceName: limitPreferenceName,
+                                       collapsedPreferenceName: activeFiltersCollapsedName,
+                                       normalizeTarget: specialPage === 'Recentchangeslinked'
                                }
                        );
 
-                       // Remove the -loading class that may have been added on the server side.
-                       // If we are in fact going to load a default saved query, this .initialize()
-                       // call will do that and add the -loading class right back.
-                       $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
-
-                       controller.initialize(
-                               mw.config.get( 'wgStructuredChangeFilters' ),
-                               // All namespaces without Media namespace
-                               rcfilters.getNamespaces( [ 'Media' ] ),
-                               mw.config.get( 'wgRCFiltersChangeTags' ),
-                               conditionalViews
-                       );
+               // TODO: The changesListWrapperWidget should be able to initialize
+               // after the model is ready.
+
+               if ( specialPage === 'Recentchanges' ) {
+                       $topSection = $( '.mw-recentchanges-toplinks' ).detach();
+               } else if ( specialPage === 'Watchlist' ) {
+                       $( '#contentSub, form#mw-watchlist-resetbutton' ).remove();
+                       $topSection = $( '.watchlistDetails' ).detach().contents();
+               } else if ( specialPage === 'Recentchangeslinked' ) {
+                       conditionalViews.recentChangesLinked = {
+                               groups: [
+                                       {
+                                               name: 'page',
+                                               type: 'any_value',
+                                               title: '',
+                                               hidden: true,
+                                               sticky: true,
+                                               filters: [
+                                                       {
+                                                               name: 'target',
+                                                               default: ''
+                                                       }
+                                               ]
+                                       },
+                                       {
+                                               name: 'toOrFrom',
+                                               type: 'boolean',
+                                               title: '',
+                                               hidden: true,
+                                               sticky: true,
+                                               filters: [
+                                                       {
+                                                               name: 'showlinkedto',
+                                                               default: false
+                                                       }
+                                               ]
+                                       }
+                               ]
+                       };
+               }
+
+               mainWrapperWidget = new mw.rcfilters.ui.MainWrapperWidget(
+                       controller,
+                       filtersModel,
+                       savedQueriesModel,
+                       changesListModel,
+                       {
+                               $wrapper: $( 'body' ),
+                               $topSection: $topSection,
+                               $filtersContainer: $( '.rcfilters-container' ),
+                               $changesListContainer: $( '.mw-changeslist, .mw-changeslist-empty' ),
+                               $formContainer: $initialFieldset,
+                               collapsed: initialCollapsedState
+                       }
+               );
 
-                       mainWrapperWidget.initFormWidget( specialPage );
+               // Remove the -loading class that may have been added on the server side.
+               // If we are in fact going to load a default saved query, this .initialize()
+               // call will do that and add the -loading class right back.
+               $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
 
-                       $( 'a.mw-helplink' ).attr(
-                               'href',
-                               'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review'
-                       );
+               controller.initialize(
+                       mw.config.get( 'wgStructuredChangeFilters' ),
+                       // All namespaces without Media namespace
+                       getNamespaces( [ 'Media' ] ),
+                       require( './config.json' ).RCFiltersChangeTags,
+                       conditionalViews
+               );
+
+               mainWrapperWidget.initFormWidget( specialPage );
 
-                       controller.replaceUrl();
+               $( 'a.mw-helplink' ).attr(
+                       'href',
+                       'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review'
+               );
 
-                       mainWrapperWidget.setTopSection( specialPage );
+               controller.replaceUrl();
 
-                       /**
-                        * Fired when initialization of the filtering interface for changes list is complete.
-                        *
-                        * @event structuredChangeFilters_ui_initialized
-                        * @member mw.hook
-                        */
-                       mw.hook( 'structuredChangeFilters.ui.initialized' ).fire();
-               },
+               mainWrapperWidget.setTopSection( specialPage );
 
                /**
-                * Get list of namespaces and remove unused ones
+                * Fired when initialization of the filtering interface for changes list is complete.
                 *
-                * @member mw.rcfilters
-                * @private
-                *
-                * @param {Array} unusedNamespaces Names of namespaces to remove
-                * @return {Array} Filtered array of namespaces
+                * @event structuredChangeFilters_ui_initialized
+                * @member mw.hook
                 */
-               getNamespaces: function ( unusedNamespaces ) {
-                       var i, length, name, id,
-                               namespaceIds = mw.config.get( 'wgNamespaceIds' ),
-                               namespaces = mw.config.get( 'wgFormattedNamespaces' );
-
-                       for ( i = 0, length = unusedNamespaces.length; i < length; i++ ) {
-                               name = unusedNamespaces[ i ];
-                               id = namespaceIds[ name.toLowerCase() ];
-                               delete namespaces[ id ];
-                       }
-
-                       return namespaces;
-               }
-       };
+               mw.hook( 'structuredChangeFilters.ui.initialized' ).fire();
+       }
 
        // Import i18n messages from config
        mw.messages.set( mw.config.get( 'wgStructuredChangeFiltersMessages' ) );
 
        // Early execute of init
        if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
-               rcfilters.init();
+               init();
        } else {
-               $( rcfilters.init );
+               $( init );
        }
 
-       module.exports = rcfilters;
+       module.exports = mw.rcfilters;
 
 }() );
index f30c278..b32fb38 100644 (file)
@@ -4,7 +4,17 @@
         * @singleton
         */
        mw.rcfilters = {
-               dm: {},
+               Controller: require( './Controller.js' ),
+               UriProcessor: require( './UriProcessor.js' ),
+               dm: {
+                       ChangesListViewModel: require( './dm/ChangesListViewModel.js' ),
+                       FilterGroup: require( './dm/FilterGroup.js' ),
+                       FilterItem: require( './dm/FilterItem.js' ),
+                       FiltersViewModel: require( './dm/FiltersViewModel.js' ),
+                       ItemModel: require( './dm/ItemModel.js' ),
+                       SavedQueriesModel: require( './dm/SavedQueriesModel.js' ),
+                       SavedQueryItemModel: require( './dm/SavedQueryItemModel.js' )
+               },
                ui: {},
                utils: {
                        addArrayElementsUnique: function ( arr, elements ) {
@@ -46,4 +56,6 @@
                        }
                }
        };
+
+       module.exports = mw.rcfilters;
 }() );
diff --git a/resources/src/mediawiki.rcfilters/ui/ChangesLimitAndDateButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/ChangesLimitAndDateButtonWidget.js
new file mode 100644 (file)
index 0000000..23b05e8
--- /dev/null
@@ -0,0 +1,174 @@
+( function () {
+       var ChangesLimitPopupWidget = require( './ChangesLimitPopupWidget.js' ),
+               DatePopupWidget = require( './DatePopupWidget.js' ),
+               ChangesLimitAndDateButtonWidget;
+
+       /**
+        * Widget defining the button controlling the popup for the number of results
+        *
+        * @class mw.rcfilters.ui.ChangesLimitAndDateButtonWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       ChangesLimitAndDateButtonWidget = function MwRcfiltersUiChangesLimitWidget( controller, model, config ) {
+               config = config || {};
+
+               // Parent
+               ChangesLimitAndDateButtonWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+
+               this.$overlay = config.$overlay || this.$element;
+
+               this.button = null;
+               this.limitGroupModel = null;
+               this.groupByPageItemModel = null;
+               this.daysGroupModel = null;
+
+               this.model.connect( this, {
+                       initialize: 'onModelInitialize'
+               } );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-changesLimitAndDateButtonWidget' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( ChangesLimitAndDateButtonWidget, OO.ui.Widget );
+
+       /**
+        * Respond to model initialize event
+        */
+       ChangesLimitAndDateButtonWidget.prototype.onModelInitialize = function () {
+               var changesLimitPopupWidget, selectedItem, currentValue, datePopupWidget,
+                       displayGroupModel = this.model.getGroup( 'display' );
+
+               this.limitGroupModel = this.model.getGroup( 'limit' );
+               this.groupByPageItemModel = displayGroupModel.getItemByParamName( 'enhanced' );
+               this.daysGroupModel = this.model.getGroup( 'days' );
+
+               // HACK: We need the model to be ready before we populate the button
+               // and the widget, because we require the filter items for the
+               // limit and their events. This addition is only done after the
+               // model is initialized.
+               // Note: This will be fixed soon!
+               if ( this.limitGroupModel && this.daysGroupModel ) {
+                       changesLimitPopupWidget = new ChangesLimitPopupWidget(
+                               this.limitGroupModel,
+                               this.groupByPageItemModel
+                       );
+
+                       datePopupWidget = new DatePopupWidget(
+                               this.daysGroupModel,
+                               {
+                                       label: mw.msg( 'rcfilters-date-popup-title' )
+                               }
+                       );
+
+                       selectedItem = this.limitGroupModel.findSelectedItems()[ 0 ];
+                       currentValue = ( selectedItem && selectedItem.getLabel() ) ||
+                               mw.language.convertNumber( this.limitGroupModel.getDefaultParamValue() );
+
+                       this.button = new OO.ui.PopupButtonWidget( {
+                               icon: 'settings',
+                               indicator: 'down',
+                               label: mw.msg( 'rcfilters-limit-and-date-label', currentValue ),
+                               $overlay: this.$overlay,
+                               popup: {
+                                       width: 300,
+                                       padded: false,
+                                       anchor: false,
+                                       align: 'backwards',
+                                       $autoCloseIgnore: this.$overlay,
+                                       $content: $( '<div>' ).append(
+                                               // TODO: Merge ChangesLimitPopupWidget with DatePopupWidget into one common widget
+                                               changesLimitPopupWidget.$element,
+                                               datePopupWidget.$element
+                                       )
+                               }
+                       } );
+                       this.updateButtonLabel();
+
+                       // Events
+                       this.limitGroupModel.connect( this, { update: 'updateButtonLabel' } );
+                       this.daysGroupModel.connect( this, { update: 'updateButtonLabel' } );
+                       changesLimitPopupWidget.connect( this, {
+                               limit: 'onPopupLimit',
+                               groupByPage: 'onPopupGroupByPage'
+                       } );
+                       datePopupWidget.connect( this, { days: 'onPopupDays' } );
+
+                       this.$element.append( this.button.$element );
+               }
+       };
+
+       /**
+        * Respond to popup limit change event
+        *
+        * @param {string} filterName Chosen filter name
+        */
+       ChangesLimitAndDateButtonWidget.prototype.onPopupLimit = function ( filterName ) {
+               var item = this.limitGroupModel.getItemByName( filterName );
+
+               this.controller.toggleFilterSelect( filterName, true );
+               this.controller.updateLimitDefault( item.getParamName() );
+               this.button.popup.toggle( false );
+       };
+
+       /**
+        * Respond to popup limit change event
+        *
+        * @param {boolean} isGrouped The result set is grouped by page
+        */
+       ChangesLimitAndDateButtonWidget.prototype.onPopupGroupByPage = function ( isGrouped ) {
+               this.controller.toggleFilterSelect( this.groupByPageItemModel.getName(), isGrouped );
+               this.controller.updateGroupByPageDefault( isGrouped );
+               this.button.popup.toggle( false );
+       };
+
+       /**
+        * Respond to popup limit change event
+        *
+        * @param {string} filterName Chosen filter name
+        */
+       ChangesLimitAndDateButtonWidget.prototype.onPopupDays = function ( filterName ) {
+               var item = this.daysGroupModel.getItemByName( filterName );
+
+               this.controller.toggleFilterSelect( filterName, true );
+               this.controller.updateDaysDefault( item.getParamName() );
+               this.button.popup.toggle( false );
+       };
+
+       /**
+        * Respond to limit choose event
+        *
+        * @param {string} filterName Filter name
+        */
+       ChangesLimitAndDateButtonWidget.prototype.updateButtonLabel = function () {
+               var message,
+                       limit = this.limitGroupModel.findSelectedItems()[ 0 ],
+                       label = limit && limit.getLabel(),
+                       days = this.daysGroupModel.findSelectedItems()[ 0 ],
+                       daysParamName = Number( days.getParamName() ) < 1 ?
+                               'rcfilters-days-show-hours' :
+                               'rcfilters-days-show-days';
+
+               // Update the label
+               if ( label && days ) {
+                       message = mw.msg( 'rcfilters-limit-and-date-label', label,
+                               mw.msg( daysParamName, days.getLabel() )
+                       );
+                       this.button.setLabel( message );
+               }
+       };
+
+       module.exports = ChangesLimitAndDateButtonWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/ChangesLimitPopupWidget.js b/resources/src/mediawiki.rcfilters/ui/ChangesLimitPopupWidget.js
new file mode 100644 (file)
index 0000000..d78c42b
--- /dev/null
@@ -0,0 +1,84 @@
+( function () {
+       var ValuePickerWidget = require( './ValuePickerWidget.js' ),
+               ChangesLimitPopupWidget;
+
+       /**
+        * Widget defining the popup to choose number of results
+        *
+        * @class mw.rcfilters.ui.ChangesLimitPopupWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FilterGroup} limitModel Group model for 'limit'
+        * @param {mw.rcfilters.dm.FilterItem} groupByPageItemModel Group model for 'limit'
+        * @param {Object} [config] Configuration object
+        */
+       ChangesLimitPopupWidget = function MwRcfiltersUiChangesLimitPopupWidget( limitModel, groupByPageItemModel, config ) {
+               config = config || {};
+
+               // Parent
+               ChangesLimitPopupWidget.parent.call( this, config );
+
+               this.limitModel = limitModel;
+               this.groupByPageItemModel = groupByPageItemModel;
+
+               this.valuePicker = new ValuePickerWidget(
+                       this.limitModel,
+                       {
+                               label: mw.msg( 'rcfilters-limit-title' )
+                       }
+               );
+
+               this.groupByPageCheckbox = new OO.ui.CheckboxInputWidget( {
+                       selected: this.groupByPageItemModel.isSelected()
+               } );
+
+               // Events
+               this.valuePicker.connect( this, { choose: [ 'emit', 'limit' ] } );
+               this.groupByPageCheckbox.connect( this, { change: [ 'emit', 'groupByPage' ] } );
+               this.groupByPageItemModel.connect( this, { update: 'onGroupByPageModelUpdate' } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
+                       .append(
+                               this.valuePicker.$element,
+                               new OO.ui.FieldLayout(
+                                       this.groupByPageCheckbox,
+                                       {
+                                               align: 'inline',
+                                               label: mw.msg( 'rcfilters-group-results-by-page' )
+                                       }
+                               ).$element
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( ChangesLimitPopupWidget, OO.ui.Widget );
+
+       /* Events */
+
+       /**
+        * @event limit
+        * @param {string} name Item name
+        *
+        * A limit item was chosen
+        */
+
+       /**
+        * @event groupByPage
+        * @param {boolean} isGrouped The results are grouped by page
+        *
+        * Results are grouped by page
+        */
+
+       /**
+        * Respond to group by page model update
+        */
+       ChangesLimitPopupWidget.prototype.onGroupByPageModelUpdate = function () {
+               this.groupByPageCheckbox.setSelected( this.groupByPageItemModel.isSelected() );
+       };
+
+       module.exports = ChangesLimitPopupWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/ChangesListWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/ChangesListWrapperWidget.js
new file mode 100644 (file)
index 0000000..361fe31
--- /dev/null
@@ -0,0 +1,388 @@
+( function () {
+       /**
+        * List of changes
+        *
+        * @class mw.rcfilters.ui.ChangesListWrapperWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
+        * @param {mw.rcfilters.Controller} controller
+        * @param {jQuery} $changesListRoot Root element of the changes list to attach to
+        * @param {Object} [config] Configuration object
+        */
+       var ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
+               filtersViewModel,
+               changesListViewModel,
+               controller,
+               $changesListRoot,
+               config
+       ) {
+               config = $.extend( {}, config, {
+                       $element: $changesListRoot
+               } );
+
+               // Parent
+               ChangesListWrapperWidget.parent.call( this, config );
+
+               this.filtersViewModel = filtersViewModel;
+               this.changesListViewModel = changesListViewModel;
+               this.controller = controller;
+               this.highlightClasses = null;
+
+               // Events
+               this.filtersViewModel.connect( this, {
+                       itemUpdate: 'onItemUpdate',
+                       highlightChange: 'onHighlightChange'
+               } );
+               this.changesListViewModel.connect( this, {
+                       invalidate: 'onModelInvalidate',
+                       update: 'onModelUpdate'
+               } );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget' )
+                       // We handle our own display/hide of the empty results message
+                       // We keep the timeout class here and remove it later, since at this
+                       // stage it is still needed to identify that the timeout occurred.
+                       .removeClass( 'mw-changeslist-empty' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( ChangesListWrapperWidget, OO.ui.Widget );
+
+       /**
+        * Get all available highlight classes
+        *
+        * @return {string[]} An array of available highlight class names
+        */
+       ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
+               if ( !this.highlightClasses || !this.highlightClasses.length ) {
+                       this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
+                               .map( function ( filterItem ) {
+                                       return filterItem.getCssClass();
+                               } );
+               }
+
+               return this.highlightClasses;
+       };
+
+       /**
+        * Respond to the highlight feature being toggled on and off
+        *
+        * @param {boolean} highlightEnabled
+        */
+       ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+               if ( highlightEnabled ) {
+                       this.applyHighlight();
+               } else {
+                       this.clearHighlight();
+               }
+       };
+
+       /**
+        * Respond to a filter item model update
+        */
+       ChangesListWrapperWidget.prototype.onItemUpdate = function () {
+               if ( this.controller.isInitialized() && this.filtersViewModel.isHighlightEnabled() ) {
+                       // this.controller.isInitialized() is still false during page load,
+                       // we don't want to clear/apply highlights at this stage.
+                       this.clearHighlight();
+                       this.applyHighlight();
+               }
+       };
+
+       /**
+        * Respond to changes list model invalidate
+        */
+       ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
+               $( 'body' ).addClass( 'mw-rcfilters-ui-loading' );
+       };
+
+       /**
+        * Respond to changes list model update
+        *
+        * @param {jQuery|string} $changesListContent The content of the updated changes list
+        * @param {jQuery} $fieldset The content of the updated fieldset
+        * @param {string} noResultsDetails Type of no result error
+        * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
+        * @param {boolean} from Timestamp of the new changes
+        */
+       ChangesListWrapperWidget.prototype.onModelUpdate = function (
+               $changesListContent, $fieldset, noResultsDetails, isInitialDOM, from
+       ) {
+               var conflictItem,
+                       $message = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
+                       isEmpty = $changesListContent === 'NO_RESULTS',
+                       // For enhanced mode, we have to load these modules, which are
+                       // not loaded for the 'regular' mode in the backend
+                       loaderPromise = mw.user.options.get( 'usenewrc' ) ?
+                               mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
+                               $.Deferred().resolve(),
+                       widget = this;
+
+               this.$element.toggleClass( 'mw-changeslist', !isEmpty );
+               if ( isEmpty ) {
+                       this.$element.empty();
+
+                       if ( this.filtersViewModel.hasConflict() ) {
+                               conflictItem = this.filtersViewModel.getFirstConflictedItem();
+
+                               $message
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-conflict' )
+                                                       .text( mw.message( 'rcfilters-noresults-conflict' ).text() ),
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-message' )
+                                                       .text( mw.message( conflictItem.getCurrentConflictResultMessage() ).text() )
+                                       );
+                       } else {
+                               $message
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-noresult' )
+                                                       .text( mw.msg( this.getMsgKeyForNoResults( noResultsDetails ) ) )
+                                       );
+
+                               // remove all classes matching mw-changeslist-*
+                               this.$element.removeClass( function ( elementIndex, allClasses ) {
+                                       return allClasses
+                                               .split( ' ' )
+                                               .filter( function ( className ) {
+                                                       return className.indexOf( 'mw-changeslist-' ) === 0;
+                                               } )
+                                               .join( ' ' );
+                               } );
+                       }
+
+                       this.$element.append( $message );
+               } else {
+                       if ( !isInitialDOM ) {
+                               this.$element.empty().append( $changesListContent );
+
+                               if ( from ) {
+                                       this.emphasizeNewChanges( from );
+                               }
+                       }
+
+                       // Apply highlight
+                       this.applyHighlight();
+
+               }
+
+               this.$element.prepend( $( '<div>' ).addClass( 'mw-changeslist-overlay' ) );
+
+               loaderPromise.done( function () {
+                       if ( !isInitialDOM && !isEmpty ) {
+                               // Make sure enhanced RC re-initializes correctly
+                               mw.hook( 'wikipage.content' ).fire( widget.$element );
+                       }
+
+                       $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
+               } );
+       };
+
+       /** Toggles overlay class on changes list
+        *
+        * @param {boolean} isVisible True if overlay should be visible
+        */
+       ChangesListWrapperWidget.prototype.toggleOverlay = function ( isVisible ) {
+               this.$element.toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget--overlaid', isVisible );
+       };
+
+       /**
+        * Map a reason for having no results to its message key
+        *
+        * @param {string} reason One of the NO_RESULTS_* "constant" that represent
+        *   a reason for having no results
+        * @return {string} Key for the message that explains why there is no results in this case
+        */
+       ChangesListWrapperWidget.prototype.getMsgKeyForNoResults = function ( reason ) {
+               var reasonMsgKeyMap = {
+                       NO_RESULTS_NORMAL: 'recentchanges-noresult',
+                       NO_RESULTS_TIMEOUT: 'recentchanges-timeout',
+                       NO_RESULTS_NETWORK_ERROR: 'recentchanges-network',
+                       NO_RESULTS_NO_TARGET_PAGE: 'recentchanges-notargetpage',
+                       NO_RESULTS_INVALID_TARGET_PAGE: 'allpagesbadtitle'
+               };
+               return reasonMsgKeyMap[ reason ];
+       };
+
+       /**
+        * Emphasize the elements (or groups) newer than the 'from' parameter
+        * @param {string} from Anything newer than this is considered 'new'
+        */
+       ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
+               var $firstNew,
+                       $indicator,
+                       $newChanges = $( [] ),
+                       selector = this.inEnhancedMode() ?
+                               'table.mw-enhanced-rc[data-mw-ts]' :
+                               'li[data-mw-ts]',
+                       set = this.$element.find( selector ),
+                       length = set.length;
+
+               set.each( function ( index ) {
+                       var $this = $( this ),
+                               ts = $this.data( 'mw-ts' );
+
+                       if ( ts >= from ) {
+                               $newChanges = $newChanges.add( $this );
+                               $firstNew = $this;
+
+                               // guards against putting the marker after the last element
+                               if ( index === ( length - 1 ) ) {
+                                       $firstNew = null;
+                               }
+                       }
+               } );
+
+               if ( $firstNew ) {
+                       $indicator = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
+
+                       $firstNew.after( $indicator );
+               }
+
+               // FIXME: Use CSS transition
+               // eslint-disable-next-line jquery/no-fade
+               $newChanges
+                       .hide()
+                       .fadeIn( 1000 );
+       };
+
+       /**
+        * In enhanced mode, we need to check whether the grouped results all have the
+        * same active highlights in order to see whether the "parent" of the group should
+        * be grey or highlighted normally.
+        *
+        * This is called every time highlights are applied.
+        */
+       ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
+               var activeHighlightClasses,
+                       $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
+
+               activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
+                       return 'mw-rcfilters-highlight-color-' + color;
+               } );
+
+               // Go over top pages and their children, and figure out if all sub-pages have the
+               // same highlights between themselves. If they do, the parent should be highlighted
+               // with all colors. If classes are different, the parent should receive a grey
+               // background
+               $enhancedTopPageCell.each( function () {
+                       var firstChildClasses, $rowsWithDifferentHighlights,
+                               $table = $( this );
+
+                       // Collect the relevant classes from the first nested child
+                       firstChildClasses = activeHighlightClasses.filter( function ( className ) {
+                               return $table.find( 'tr:nth-child(2)' ).hasClass( className );
+                       } );
+                       // Filter the non-head rows and see if they all have the same classes
+                       // to the first row
+                       $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
+                               var classesInThisRow,
+                                       $this = $( this );
+
+                               classesInThisRow = activeHighlightClasses.filter( function ( className ) {
+                                       return $this.hasClass( className );
+                               } );
+
+                               return !OO.compare( firstChildClasses, classesInThisRow );
+                       } );
+
+                       // If classes are different, tag the row for using grey color
+                       $table.find( 'tr:first-child' )
+                               .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
+               } );
+       };
+
+       /**
+        * @return {boolean} Whether the changes are grouped by page
+        */
+       ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
+               var uri = new mw.Uri();
+               return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
+                       ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
+       };
+
+       /**
+        * Apply color classes based on filters highlight configuration
+        */
+       ChangesListWrapperWidget.prototype.applyHighlight = function () {
+               if ( !this.filtersViewModel.isHighlightEnabled() ) {
+                       return;
+               }
+
+               this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
+                       var $elements = this.$element.find( '.' + filterItem.getCssClass() );
+
+                       // Add highlight class to all highlighted list items
+                       $elements
+                               .addClass(
+                                       'mw-rcfilters-highlighted ' +
+                                       'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor()
+                               );
+
+                       // Track the filters for each item in .data( 'highlightedFilters' )
+                       $elements.each( function () {
+                               var filters = $( this ).data( 'highlightedFilters' );
+                               if ( !filters ) {
+                                       filters = [];
+                                       $( this ).data( 'highlightedFilters', filters );
+                               }
+                               if ( filters.indexOf( filterItem.getLabel() ) === -1 ) {
+                                       filters.push( filterItem.getLabel() );
+                               }
+                       } );
+               }.bind( this ) );
+               // Apply a title to each highlighted item, with a list of filters
+               this.$element.find( '.mw-rcfilters-highlighted' ).each( function () {
+                       var filters = $( this ).data( 'highlightedFilters' );
+
+                       if ( filters && filters.length ) {
+                               $( this ).attr( 'title', mw.msg(
+                                       'rcfilters-highlighted-filters-list',
+                                       filters.join( mw.msg( 'comma-separator' ) )
+                               ) );
+                       }
+
+               } );
+               if ( this.inEnhancedMode() ) {
+                       this.updateEnhancedParentHighlight();
+               }
+
+               // Turn on highlights
+               this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+       };
+
+       /**
+        * Remove all color classes
+        */
+       ChangesListWrapperWidget.prototype.clearHighlight = function () {
+               // Remove highlight classes
+               mw.rcfilters.HighlightColors.forEach( function ( color ) {
+                       this.$element
+                               .find( '.mw-rcfilters-highlight-color-' + color )
+                               .removeClass( 'mw-rcfilters-highlight-color-' + color );
+               }.bind( this ) );
+
+               this.$element.find( '.mw-rcfilters-highlighted' )
+                       .removeAttr( 'title' )
+                       .removeData( 'highlightedFilters' )
+                       .removeClass( 'mw-rcfilters-highlighted' );
+
+               // Remove grey from enhanced rows
+               this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
+                       .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
+
+               // Turn off highlights
+               this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+       };
+
+       module.exports = ChangesListWrapperWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/CheckboxInputWidget.js b/resources/src/mediawiki.rcfilters/ui/CheckboxInputWidget.js
new file mode 100644 (file)
index 0000000..490d54e
--- /dev/null
@@ -0,0 +1,66 @@
+( function () {
+       /**
+        * A widget representing a single toggle filter
+        *
+        * @class mw.rcfilters.ui.CheckboxInputWidget
+        * @extends OO.ui.CheckboxInputWidget
+        *
+        * @constructor
+        * @param {Object} config Configuration object
+        */
+       var CheckboxInputWidget = function MwRcfiltersUiCheckboxInputWidget( config ) {
+               config = config || {};
+
+               // Parent
+               CheckboxInputWidget.parent.call( this, config );
+
+               // Event
+               this.$input
+                       // HACK: This widget just pretends to be a checkbox for visual purposes.
+                       // In reality, all actions - setting to true or false, etc - are
+                       // decided by the model, and executed by the controller. This means
+                       // that we want to let the controller and model make the decision
+                       // of whether to check/uncheck this checkboxInputWidget, and for that,
+                       // we have to bypass the browser action that checks/unchecks it during
+                       // click.
+                       .on( 'click', false )
+                       .on( 'change', this.onUserChange.bind( this ) );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( CheckboxInputWidget, OO.ui.CheckboxInputWidget );
+
+       /* Events */
+
+       /**
+        * @event userChange
+        * @param {boolean} Current state of the checkbox
+        *
+        * The user has checked or unchecked this checkbox
+        */
+
+       /* Methods */
+
+       /**
+        * @inheritdoc
+        */
+       CheckboxInputWidget.prototype.onEdit = function () {
+               // Similarly to preventing defaults in 'click' event, we want
+               // to prevent this widget from deciding anything about its own
+               // state; it emits a change event and the model and controller
+               // make a decision about what its select state is.
+               // onEdit has a widget.$input.prop( 'checked' ) inside a setTimeout()
+               // so we really want to prevent that from messing with what
+               // the model decides the state of the widget is.
+       };
+
+       /**
+        * Respond to checkbox change by a user and emit 'userChange'.
+        */
+       CheckboxInputWidget.prototype.onUserChange = function () {
+               this.emit( 'userChange', this.$input.prop( 'checked' ) );
+       };
+
+       module.exports = CheckboxInputWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/DatePopupWidget.js b/resources/src/mediawiki.rcfilters/ui/DatePopupWidget.js
new file mode 100644 (file)
index 0000000..1ac0d49
--- /dev/null
@@ -0,0 +1,72 @@
+( function () {
+       var ValuePickerWidget = require( './ValuePickerWidget.js' ),
+               DatePopupWidget;
+
+       /**
+        * Widget defining the popup to choose date for the results
+        *
+        * @class mw.rcfilters.ui.DatePopupWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'days'
+        * @param {Object} [config] Configuration object
+        */
+       DatePopupWidget = function MwRcfiltersUiDatePopupWidget( model, config ) {
+               config = config || {};
+
+               // Parent
+               DatePopupWidget.parent.call( this, config );
+               // Mixin constructors
+               OO.ui.mixin.LabelElement.call( this, config );
+
+               this.model = model;
+
+               this.hoursValuePicker = new ValuePickerWidget(
+                       this.model,
+                       {
+                               classes: [ 'mw-rcfilters-ui-datePopupWidget-hours' ],
+                               label: mw.msg( 'rcfilters-hours-title' ),
+                               itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) < 1; }
+                       }
+               );
+               this.daysValuePicker = new ValuePickerWidget(
+                       this.model,
+                       {
+                               classes: [ 'mw-rcfilters-ui-datePopupWidget-days' ],
+                               label: mw.msg( 'rcfilters-days-title' ),
+                               itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) >= 1; }
+                       }
+               );
+
+               // Events
+               this.hoursValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
+               this.daysValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-datePopupWidget' )
+                       .append(
+                               this.$label
+                                       .addClass( 'mw-rcfilters-ui-datePopupWidget-title' ),
+                               this.hoursValuePicker.$element,
+                               this.daysValuePicker.$element
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( DatePopupWidget, OO.ui.Widget );
+       OO.mixinClass( DatePopupWidget, OO.ui.mixin.LabelElement );
+
+       /* Events */
+
+       /**
+        * @event days
+        * @param {string} name Item name
+        *
+        * A days item was chosen
+        */
+
+       module.exports = DatePopupWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterItemHighlightButton.js b/resources/src/mediawiki.rcfilters/ui/FilterItemHighlightButton.js
new file mode 100644 (file)
index 0000000..1327755
--- /dev/null
@@ -0,0 +1,85 @@
+( function () {
+       /**
+        * A button to configure highlight for a filter item
+        *
+        * @class mw.rcfilters.ui.FilterItemHighlightButton
+        * @extends OO.ui.PopupButtonWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+        * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
+        * @param {Object} [config] Configuration object
+        */
+       var FilterItemHighlightButton = function MwRcfiltersUiFilterItemHighlightButton( controller, model, highlightPopup, config ) {
+               config = config || {};
+
+               // Parent
+               FilterItemHighlightButton.parent.call( this, $.extend( true, {}, config, {
+                       icon: 'highlight',
+                       indicator: 'down'
+               } ) );
+
+               this.controller = controller;
+               this.model = model;
+               this.popup = highlightPopup;
+
+               // Event
+               this.model.connect( this, { update: 'updateUiBasedOnModel' } );
+               // This lives inside a MenuOptionWidget, which intercepts mousedown
+               // to select the item. We want to prevent that when we click the highlight
+               // button
+               this.$element.on( 'mousedown', function ( e ) {
+                       e.stopPropagation();
+               } );
+
+               this.updateUiBasedOnModel();
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FilterItemHighlightButton, OO.ui.PopupButtonWidget );
+
+       /* Static Properties */
+
+       /**
+        * @static
+        */
+       FilterItemHighlightButton.static.cancelButtonMouseDownEvents = true;
+
+       /* Methods */
+
+       FilterItemHighlightButton.prototype.onAction = function () {
+               this.popup.setAssociatedButton( this );
+               this.popup.setFilterItem( this.model );
+
+               // Parent method
+               FilterItemHighlightButton.parent.prototype.onAction.call( this );
+       };
+
+       /**
+        * Respond to item model update event
+        */
+       FilterItemHighlightButton.prototype.updateUiBasedOnModel = function () {
+               var currentColor = this.model.getHighlightColor(),
+                       widget = this;
+
+               this.$icon.toggleClass(
+                       'mw-rcfilters-ui-filterItemHighlightButton-circle',
+                       currentColor !== null
+               );
+
+               mw.rcfilters.HighlightColors.forEach( function ( c ) {
+                       widget.$icon
+                               .toggleClass(
+                                       'mw-rcfilters-ui-filterItemHighlightButton-circle-color-' + c,
+                                       c === currentColor
+                               );
+               } );
+       };
+
+       module.exports = FilterItemHighlightButton;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterMenuHeaderWidget.js b/resources/src/mediawiki.rcfilters/ui/FilterMenuHeaderWidget.js
new file mode 100644 (file)
index 0000000..1396341
--- /dev/null
@@ -0,0 +1,184 @@
+( function () {
+       /**
+        * Menu header for the RCFilters filters menu
+        *
+        * @class mw.rcfilters.ui.FilterMenuHeaderWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       var FilterMenuHeaderWidget = function MwRcfiltersUiFilterMenuHeaderWidget( controller, model, config ) {
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               // Parent
+               FilterMenuHeaderWidget.parent.call( this, config );
+               OO.ui.mixin.LabelElement.call( this, $.extend( {
+                       label: mw.msg( 'rcfilters-filterlist-title' ),
+                       $label: $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-title' )
+               }, config ) );
+
+               // "Back" to default view button
+               this.backButton = new OO.ui.ButtonWidget( {
+                       icon: 'previous',
+                       framed: false,
+                       title: mw.msg( 'rcfilters-view-return-to-default-tooltip' ),
+                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-backButton' ]
+               } );
+               this.backButton.toggle( this.model.getCurrentView() !== 'default' );
+
+               // Help icon for Tagged edits
+               this.helpIcon = new OO.ui.ButtonWidget( {
+                       icon: 'helpNotice',
+                       framed: false,
+                       title: mw.msg( 'rcfilters-view-tags-help-icon-tooltip' ),
+                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-helpIcon' ],
+                       href: mw.util.getUrl( 'Special:Tags' ),
+                       target: '_blank'
+               } );
+               this.helpIcon.toggle( this.model.getCurrentView() === 'tags' );
+
+               // Highlight button
+               this.highlightButton = new OO.ui.ToggleButtonWidget( {
+                       icon: 'highlight',
+                       label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
+                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-hightlightButton' ]
+               } );
+
+               // Invert namespaces button
+               this.invertNamespacesButton = new OO.ui.ToggleButtonWidget( {
+                       icon: '',
+                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-invertNamespacesButton' ]
+               } );
+               this.invertNamespacesButton.toggle( this.model.getCurrentView() === 'namespaces' );
+
+               // Events
+               this.backButton.connect( this, { click: 'onBackButtonClick' } );
+               this.highlightButton
+                       .connect( this, { click: 'onHighlightButtonClick' } );
+               this.invertNamespacesButton
+                       .connect( this, { click: 'onInvertNamespacesButtonClick' } );
+               this.model.connect( this, {
+                       highlightChange: 'onModelHighlightChange',
+                       searchChange: 'onModelSearchChange',
+                       initialize: 'onModelInitialize'
+               } );
+               this.view = this.model.getCurrentView();
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-back' )
+                                                                       .append( this.backButton.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' )
+                                                                       .append( this.$label, this.helpIcon.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-invert' )
+                                                                       .append( this.invertNamespacesButton.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-highlight' )
+                                                                       .append( this.highlightButton.$element )
+                                                       )
+                                       )
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FilterMenuHeaderWidget, OO.ui.Widget );
+       OO.mixinClass( FilterMenuHeaderWidget, OO.ui.mixin.LabelElement );
+
+       /* Methods */
+
+       /**
+        * Respond to model initialization event
+        *
+        * Note: need to wait for initialization before getting the invertModel
+        * and registering its update event. Creating all the models before the UI
+        * would help with that.
+        */
+       FilterMenuHeaderWidget.prototype.onModelInitialize = function () {
+               this.invertModel = this.model.getInvertModel();
+               this.updateInvertButton();
+               this.invertModel.connect( this, { update: 'updateInvertButton' } );
+       };
+
+       /**
+        * Respond to model update event
+        */
+       FilterMenuHeaderWidget.prototype.onModelSearchChange = function () {
+               var currentView = this.model.getCurrentView();
+
+               if ( this.view !== currentView ) {
+                       this.setLabel( this.model.getViewTitle( currentView ) );
+
+                       this.invertNamespacesButton.toggle( currentView === 'namespaces' );
+                       this.backButton.toggle( currentView !== 'default' );
+                       this.helpIcon.toggle( currentView === 'tags' );
+                       this.view = currentView;
+               }
+       };
+
+       /**
+        * Respond to model highlight change event
+        *
+        * @param {boolean} highlightEnabled Highlight is enabled
+        */
+       FilterMenuHeaderWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
+               this.highlightButton.setActive( highlightEnabled );
+       };
+
+       /**
+        * Update the state of the invert button
+        */
+       FilterMenuHeaderWidget.prototype.updateInvertButton = function () {
+               this.invertNamespacesButton.setActive( this.invertModel.isSelected() );
+               this.invertNamespacesButton.setLabel(
+                       this.invertModel.isSelected() ?
+                               mw.msg( 'rcfilters-exclude-button-on' ) :
+                               mw.msg( 'rcfilters-exclude-button-off' )
+               );
+       };
+
+       FilterMenuHeaderWidget.prototype.onBackButtonClick = function () {
+               this.controller.switchView( 'default' );
+       };
+
+       /**
+        * Respond to highlight button click
+        */
+       FilterMenuHeaderWidget.prototype.onHighlightButtonClick = function () {
+               this.controller.toggleHighlight();
+       };
+
+       /**
+        * Respond to highlight button click
+        */
+       FilterMenuHeaderWidget.prototype.onInvertNamespacesButtonClick = function () {
+               this.controller.toggleInvertedNamespaces();
+       };
+
+       module.exports = FilterMenuHeaderWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/FilterMenuOptionWidget.js
new file mode 100644 (file)
index 0000000..4080f4d
--- /dev/null
@@ -0,0 +1,96 @@
+( function () {
+       var ItemMenuOptionWidget = require( './ItemMenuOptionWidget.js' ),
+               FilterMenuOptionWidget;
+
+       /**
+        * A widget representing a single toggle filter
+        *
+        * @class mw.rcfilters.ui.FilterMenuOptionWidget
+        * @extends mw.rcfilters.ui.ItemMenuOptionWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+        * @param {mw.rcfilters.dm.FilterItem} invertModel
+        * @param {mw.rcfilters.dm.FilterItem} itemModel Filter item model
+        * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker popup
+        * @param {Object} config Configuration object
+        */
+       FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget(
+               controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
+       ) {
+               config = config || {};
+
+               this.controller = controller;
+               this.invertModel = invertModel;
+               this.model = itemModel;
+
+               // Parent
+               FilterMenuOptionWidget.parent.call( this, controller, filtersViewModel, this.invertModel, itemModel, highlightPopup, config );
+
+               // Event
+               this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' );
+       };
+
+       /* Initialization */
+       OO.inheritClass( FilterMenuOptionWidget, ItemMenuOptionWidget );
+
+       /* Static properties */
+
+       // We do our own scrolling to top
+       FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false;
+
+       /* Methods */
+
+       /**
+        * @inheritdoc
+        */
+       FilterMenuOptionWidget.prototype.updateUiBasedOnState = function () {
+               // Parent
+               FilterMenuOptionWidget.parent.prototype.updateUiBasedOnState.call( this );
+
+               this.setCurrentMuteState();
+       };
+
+       /**
+        * Respond to item group model update event
+        */
+       FilterMenuOptionWidget.prototype.onGroupModelUpdate = function () {
+               this.setCurrentMuteState();
+       };
+
+       /**
+        * Set the current muted view of the widget based on its state
+        */
+       FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
+               if (
+                       this.model.getGroupModel().getView() === 'namespaces' &&
+                       this.invertModel.isSelected()
+               ) {
+                       // This is an inverted behavior than the other rules, specifically
+                       // for inverted namespaces
+                       this.setFlags( {
+                               muted: this.model.isSelected()
+                       } );
+               } else {
+                       this.setFlags( {
+                               muted: (
+                                       this.model.isConflicted() ||
+                                       (
+                                               // Item is also muted when any of the items in its group is active
+                                               this.model.getGroupModel().isActive() &&
+                                               // But it isn't selected
+                                               !this.model.isSelected() &&
+                                               // And also not included
+                                               !this.model.isIncluded()
+                                       )
+                               )
+                       } );
+               }
+       };
+
+       module.exports = FilterMenuOptionWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterMenuSectionOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/FilterMenuSectionOptionWidget.js
new file mode 100644 (file)
index 0000000..5b9e359
--- /dev/null
@@ -0,0 +1,127 @@
+( function () {
+       /**
+        * A widget representing a menu section for filter groups
+        *
+        * @class mw.rcfilters.ui.FilterMenuSectionOptionWidget
+        * @extends OO.ui.MenuSectionOptionWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FilterGroup} model Filter group model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] Overlay
+        */
+       var FilterMenuSectionOptionWidget = function MwRcfiltersUiFilterMenuSectionOptionWidget( controller, model, config ) {
+               var whatsThisMessages,
+                       $header = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ),
+                       $popupContent = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               // Parent
+               FilterMenuSectionOptionWidget.parent.call( this, $.extend( {
+                       label: this.model.getTitle(),
+                       $label: $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title' )
+               }, config ) );
+
+               $header.append( this.$label );
+
+               if ( this.model.hasWhatsThis() ) {
+                       whatsThisMessages = this.model.getWhatsThis();
+
+                       // Create popup
+                       if ( whatsThisMessages.header ) {
+                               $popupContent.append(
+                                       ( new OO.ui.LabelWidget( {
+                                               label: mw.msg( whatsThisMessages.header ),
+                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-header' ]
+                                       } ) ).$element
+                               );
+                       }
+                       if ( whatsThisMessages.body ) {
+                               $popupContent.append(
+                                       ( new OO.ui.LabelWidget( {
+                                               label: mw.msg( whatsThisMessages.body ),
+                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-body' ]
+                                       } ) ).$element
+                               );
+                       }
+                       if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
+                               $popupContent.append(
+                                       ( new OO.ui.ButtonWidget( {
+                                               framed: false,
+                                               flags: [ 'progressive' ],
+                                               href: whatsThisMessages.url,
+                                               label: mw.msg( whatsThisMessages.linkText ),
+                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-link' ]
+                                       } ) ).$element
+                               );
+                       }
+
+                       // Add button
+                       this.whatsThisButton = new OO.ui.PopupButtonWidget( {
+                               framed: false,
+                               label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
+                               $overlay: this.$overlay,
+                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
+                               flags: [ 'progressive' ],
+                               popup: {
+                                       padded: false,
+                                       align: 'center',
+                                       position: 'above',
+                                       $content: $popupContent,
+                                       classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup' ]
+                               }
+                       } );
+
+                       $header
+                               .append( this.whatsThisButton.$element );
+               }
+
+               // Events
+               this.model.connect( this, { update: 'updateUiBasedOnState' } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget' )
+                       .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-name-' + this.model.getName() )
+                       .append( $header );
+               this.updateUiBasedOnState();
+       };
+
+       /* Initialize */
+
+       OO.inheritClass( FilterMenuSectionOptionWidget, OO.ui.MenuSectionOptionWidget );
+
+       /* Methods */
+
+       /**
+        * Respond to model update event
+        */
+       FilterMenuSectionOptionWidget.prototype.updateUiBasedOnState = function () {
+               this.$element.toggleClass(
+                       'mw-rcfilters-ui-filterMenuSectionOptionWidget-active',
+                       this.model.isActive()
+               );
+               this.toggle( this.model.isVisible() );
+       };
+
+       /**
+        * Get the group name
+        *
+        * @return {string} Group name
+        */
+       FilterMenuSectionOptionWidget.prototype.getName = function () {
+               return this.model.getName();
+       };
+
+       module.exports = FilterMenuSectionOptionWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterTagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/FilterTagItemWidget.js
new file mode 100644 (file)
index 0000000..bda898b
--- /dev/null
@@ -0,0 +1,50 @@
+( function () {
+       var TagItemWidget = require( './TagItemWidget.js' ),
+               FilterTagItemWidget;
+
+       /**
+        * Extend OOUI's FilterTagItemWidget to also display a popup on hover.
+        *
+        * @class mw.rcfilters.ui.FilterTagItemWidget
+        * @extends mw.rcfilters.ui.TagItemWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+        * @param {mw.rcfilters.dm.FilterItem} invertModel
+        * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
+        * @param {Object} config Configuration object
+        */
+       FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget(
+               controller, filtersViewModel, invertModel, itemModel, config
+       ) {
+               config = config || {};
+
+               FilterTagItemWidget.parent.call( this, controller, filtersViewModel, invertModel, itemModel, config );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterTagItemWidget' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FilterTagItemWidget, TagItemWidget );
+
+       /* Methods */
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagItemWidget.prototype.setCurrentMuteState = function () {
+               this.setFlags( {
+                       muted: (
+                               !this.itemModel.isSelected() ||
+                               this.itemModel.isIncluded() ||
+                               this.itemModel.isFullyCovered()
+                       ),
+                       invalid: this.itemModel.isSelected() && this.itemModel.isConflicted()
+               } );
+       };
+
+       module.exports = FilterTagItemWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterTagMultiselectWidget.js b/resources/src/mediawiki.rcfilters/ui/FilterTagMultiselectWidget.js
new file mode 100644 (file)
index 0000000..4881542
--- /dev/null
@@ -0,0 +1,778 @@
+( function () {
+       var ViewSwitchWidget = require( './ViewSwitchWidget.js' ),
+               SaveFiltersPopupButtonWidget = require( './SaveFiltersPopupButtonWidget.js' ),
+               MenuSelectWidget = require( './MenuSelectWidget.js' ),
+               FilterTagItemWidget = require( './FilterTagItemWidget.js' ),
+               FilterTagMultiselectWidget;
+
+       /**
+        * List displaying all filter groups
+        *
+        * @class mw.rcfilters.ui.FilterTagMultiselectWidget
+        * @extends OO.ui.MenuTagMultiselectWidget
+        * @mixins OO.ui.mixin.PendingElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
+        *  system. If not given, falls back to this widget's $element
+        * @cfg {boolean} [collapsed] Filter area is collapsed
+        */
+       FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
+               var rcFiltersRow,
+                       title = new OO.ui.LabelWidget( {
+                               label: mw.msg( 'rcfilters-activefilters' ),
+                               classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
+                       } ),
+                       $contentWrapper = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.queriesModel = savedQueriesModel;
+               this.$overlay = config.$overlay || this.$element;
+               this.$wrapper = config.$wrapper || this.$element;
+               this.matchingQuery = null;
+               this.currentView = this.model.getCurrentView();
+               this.collapsed = false;
+
+               // Parent
+               FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
+                       label: mw.msg( 'rcfilters-filterlist-title' ),
+                       placeholder: mw.msg( 'rcfilters-empty-filter' ),
+                       inputPosition: 'outline',
+                       allowArbitrary: false,
+                       allowDisplayInvalidTags: false,
+                       allowReordering: false,
+                       $overlay: this.$overlay,
+                       menu: {
+                               // Our filtering is done through the model
+                               filterFromInput: false,
+                               hideWhenOutOfView: false,
+                               hideOnChoose: false,
+                               width: 650,
+                               footers: [
+                                       {
+                                               name: 'viewSelect',
+                                               sticky: false,
+                                               // View select menu, appears on default view only
+                                               $element: $( '<div>' )
+                                                       .append( new ViewSwitchWidget( this.controller, this.model ).$element ),
+                                               views: [ 'default' ]
+                                       },
+                                       {
+                                               name: 'feedback',
+                                               // Feedback footer, appears on all views
+                                               $element: $( '<div>' )
+                                                       .append(
+                                                               new OO.ui.ButtonWidget( {
+                                                                       framed: false,
+                                                                       icon: 'feedback',
+                                                                       flags: [ 'progressive' ],
+                                                                       label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
+                                                                       href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
+                                                               } ).$element
+                                                       )
+                                       }
+                               ]
+                       },
+                       input: {
+                               icon: 'menu',
+                               placeholder: mw.msg( 'rcfilters-search-placeholder' )
+                       }
+               }, config ) );
+
+               this.savedQueryTitle = new OO.ui.LabelWidget( {
+                       label: '',
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
+               } );
+
+               this.resetButton = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
+               } );
+
+               this.hideShowButton = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       flags: [ 'progressive' ],
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
+               } );
+               this.toggleCollapsed( !!config.collapsed );
+
+               if ( !mw.user.isAnon() ) {
+                       this.saveQueryButton = new SaveFiltersPopupButtonWidget(
+                               this.controller,
+                               this.queriesModel,
+                               {
+                                       $overlay: this.$overlay
+                               }
+                       );
+
+                       this.saveQueryButton.$element.on( 'mousedown', function ( e ) {
+                               e.stopPropagation();
+                       } );
+
+                       this.saveQueryButton.connect( this, {
+                               click: 'onSaveQueryButtonClick',
+                               saveCurrent: 'setSavedQueryVisibility'
+                       } );
+                       this.queriesModel.connect( this, {
+                               itemUpdate: 'onSavedQueriesItemUpdate',
+                               initialize: 'onSavedQueriesInitialize',
+                               default: 'reevaluateResetRestoreState'
+                       } );
+               }
+
+               this.emptyFilterMessage = new OO.ui.LabelWidget( {
+                       label: mw.msg( 'rcfilters-empty-filter' ),
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
+               } );
+               this.$content.append( this.emptyFilterMessage.$element );
+
+               // Events
+               this.resetButton.connect( this, { click: 'onResetButtonClick' } );
+               this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
+               // Stop propagation for mousedown, so that the widget doesn't
+               // trigger the focus on the input and scrolls up when we click the reset button
+               this.resetButton.$element.on( 'mousedown', function ( e ) {
+                       e.stopPropagation();
+               } );
+               this.hideShowButton.$element.on( 'mousedown', function ( e ) {
+                       e.stopPropagation();
+               } );
+               this.model.connect( this, {
+                       initialize: 'onModelInitialize',
+                       update: 'onModelUpdate',
+                       searchChange: 'onModelSearchChange',
+                       itemUpdate: 'onModelItemUpdate',
+                       highlightChange: 'onModelHighlightChange'
+               } );
+               this.input.connect( this, { change: 'onInputChange' } );
+
+               // The filter list and button should appear side by side regardless of how
+               // wide the button is; the button also changes its width depending
+               // on language and its state, so the safest way to present both side
+               // by side is with a table layout
+               rcFiltersRow = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-row' )
+                       .append(
+                               this.$content
+                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
+                       );
+
+               if ( !mw.user.isAnon() ) {
+                       rcFiltersRow.append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
+                                       .append( this.saveQueryButton.$element )
+                       );
+               }
+
+               // Add a selector at the right of the input
+               this.viewsSelectWidget = new OO.ui.ButtonSelectWidget( {
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
+                       items: [
+                               new OO.ui.ButtonOptionWidget( {
+                                       framed: false,
+                                       data: 'namespaces',
+                                       icon: 'article',
+                                       label: mw.msg( 'namespaces' ),
+                                       title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
+                               } ),
+                               new OO.ui.ButtonOptionWidget( {
+                                       framed: false,
+                                       data: 'tags',
+                                       icon: 'tag',
+                                       label: mw.msg( 'tags-title' ),
+                                       title: mw.msg( 'rcfilters-view-tags-tooltip' )
+                               } )
+                       ]
+               } );
+
+               // Rearrange the UI so the select widget is at the right of the input
+               this.$element.append(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-table' )
+                               .append(
+                                       $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-row' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
+                                               .append(
+                                                       $( '<div>' )
+                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
+                                                               .append( this.input.$element ),
+                                                       $( '<div>' )
+                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
+                                                               .append( this.viewsSelectWidget.$element )
+                                               )
+                               )
+               );
+
+               // Event
+               this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } );
+
+               rcFiltersRow.append(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-cell' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
+                               .append( this.resetButton.$element )
+               );
+
+               // Build the content
+               $contentWrapper.append(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
+                               .append(
+                                       $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
+                                               .append( title.$element ),
+                                       $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
+                                               .append( this.savedQueryTitle.$element ),
+                                       $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
+                                               .append(
+                                                       this.hideShowButton.$element
+                                               )
+                               ),
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-table' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
+                               .append( rcFiltersRow )
+               );
+
+               // Initialize
+               this.$handle.append( $contentWrapper );
+               this.emptyFilterMessage.toggle( this.isEmpty() );
+               this.savedQueryTitle.toggle( false );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
+
+               this.reevaluateResetRestoreState();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
+
+       /* Methods */
+
+       /**
+        * Override parent method to avoid unnecessary resize events.
+        */
+       FilterTagMultiselectWidget.prototype.updateIfHeightChanged = function () { };
+
+       /**
+        * Respond to view select widget choose event
+        *
+        * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget
+        */
+       FilterTagMultiselectWidget.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget ) {
+               this.controller.switchView( buttonOptionWidget.getData() );
+               this.viewsSelectWidget.selectItem( null );
+               this.focus();
+       };
+
+       /**
+        * Respond to model search change event
+        *
+        * @param {string} value Search value
+        */
+       FilterTagMultiselectWidget.prototype.onModelSearchChange = function ( value ) {
+               this.input.setValue( value );
+       };
+
+       /**
+        * Respond to input change event
+        *
+        * @param {string} value Value of the input
+        */
+       FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
+               this.controller.setSearch( value );
+       };
+
+       /**
+        * Respond to query button click
+        */
+       FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
+               this.getMenu().toggle( false );
+       };
+
+       /**
+        * Respond to save query model initialization
+        */
+       FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
+               this.setSavedQueryVisibility();
+       };
+
+       /**
+        * Respond to save query item change. Mainly this is done to update the label in case
+        * a query item has been edited
+        *
+        * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
+        */
+       FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
+               if ( this.matchingQuery === item ) {
+                       // This means we just edited the item that is currently matched
+                       this.savedQueryTitle.setLabel( item.getLabel() );
+               }
+       };
+
+       /**
+        * Respond to menu toggle
+        *
+        * @param {boolean} isVisible Menu is visible
+        */
+       FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
+               // Parent
+               FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
+
+               if ( isVisible ) {
+                       this.focus();
+
+                       mw.hook( 'RcFilters.popup.open' ).fire();
+
+                       if ( !this.getMenu().findSelectedItem() ) {
+                               // If there are no selected items, scroll menu to top
+                               // This has to be in a setTimeout so the menu has time
+                               // to be positioned and fixed
+                               setTimeout(
+                                       function () {
+                                               this.getMenu().scrollToTop();
+                                       }.bind( this )
+                               );
+                       }
+               } else {
+                       // Clear selection
+                       this.selectTag( null );
+
+                       // Clear the search
+                       this.controller.setSearch( '' );
+
+                       // Log filter grouping
+                       this.controller.trackFilterGroupings( 'filtermenu' );
+
+                       this.blur();
+               }
+
+               this.input.setIcon( isVisible ? 'search' : 'menu' );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.onInputFocus = function () {
+               // Parent
+               FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
+
+               // Only scroll to top of the viewport if:
+               // - The widget is more than 20px from the top
+               // - The widget is not above the top of the viewport (do not scroll downwards)
+               //   (This isn't represented because >20 is, anyways and always, bigger than 0)
+               this.scrollToTop( this.$element, 0, { min: 20, max: Infinity } );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.doInputEscape = function () {
+               // Parent
+               FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
+
+               // Blur the input
+               this.input.$input.trigger( 'blur' );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
+               if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+                       this.menu.toggle();
+
+                       return false;
+               }
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.onChangeTags = function () {
+               // If initialized, call parent method.
+               if ( this.controller.isInitialized() ) {
+                       FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
+               }
+
+               this.emptyFilterMessage.toggle( this.isEmpty() );
+       };
+
+       /**
+        * Respond to model initialize event
+        */
+       FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
+               this.setSavedQueryVisibility();
+       };
+
+       /**
+        * Respond to model update event
+        */
+       FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
+               this.updateElementsForView();
+       };
+
+       /**
+        * Update the elements in the widget to the current view
+        */
+       FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
+               var view = this.model.getCurrentView(),
+                       inputValue = this.input.getValue().trim(),
+                       inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) );
+
+               if ( inputView !== 'default' ) {
+                       // We have a prefix already, remove it
+                       inputValue = inputValue.substr( 1 );
+               }
+
+               if ( inputView !== view ) {
+                       // Add the correct prefix
+                       inputValue = this.model.getViewTrigger( view ) + inputValue;
+               }
+
+               // Update input
+               this.input.setValue( inputValue );
+
+               if ( this.currentView !== view ) {
+                       this.scrollToTop( this.$element );
+                       this.currentView = view;
+               }
+       };
+
+       /**
+        * Set the visibility of the saved query button
+        */
+       FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
+               if ( mw.user.isAnon() ) {
+                       return;
+               }
+
+               this.matchingQuery = this.controller.findQueryMatchingCurrentState();
+
+               this.savedQueryTitle.setLabel(
+                       this.matchingQuery ? this.matchingQuery.getLabel() : ''
+               );
+               this.savedQueryTitle.toggle( !!this.matchingQuery );
+               this.saveQueryButton.setDisabled( !!this.matchingQuery );
+               this.saveQueryButton.setTitle( !this.matchingQuery ?
+                       mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
+                       mw.msg( 'rcfilters-savedqueries-already-saved' ) );
+
+               if ( this.matchingQuery ) {
+                       this.emphasize();
+               }
+       };
+
+       /**
+        * Respond to model itemUpdate event
+        * fixme: when a new state is applied to the model this function is called 60+ times in a row
+        *
+        * @param {mw.rcfilters.dm.FilterItem} item Filter item model
+        */
+       FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
+               if ( !item.getGroupModel().isHidden() ) {
+                       if (
+                               item.isSelected() ||
+                               (
+                                       this.model.isHighlightEnabled() &&
+                                       item.getHighlightColor()
+                               )
+                       ) {
+                               this.addTag( item.getName(), item.getLabel() );
+                       } else {
+                               // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
+                               if ( this.findItemFromData( item.getName() ) !== null ) {
+                                       this.removeTagByData( item.getName() );
+                               }
+                       }
+               }
+
+               this.setSavedQueryVisibility();
+
+               // Re-evaluate reset state
+               this.reevaluateResetRestoreState();
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
+               return (
+                       this.model.getItemByName( data ) &&
+                       !this.isDuplicateData( data )
+               );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
+               this.controller.toggleFilterSelect( item.model.getName() );
+
+               // Select the tag if it exists, or reset selection otherwise
+               this.selectTag( this.findItemFromData( item.model.getName() ) );
+
+               this.focus();
+       };
+
+       /**
+        * Respond to highlightChange event
+        *
+        * @param {boolean} isHighlightEnabled Highlight is enabled
+        */
+       FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
+               var highlightedItems = this.model.getHighlightedItems();
+
+               if ( isHighlightEnabled ) {
+                       // Add capsule widgets
+                       highlightedItems.forEach( function ( filterItem ) {
+                               this.addTag( filterItem.getName(), filterItem.getLabel() );
+                       }.bind( this ) );
+               } else {
+                       // Remove capsule widgets if they're not selected
+                       highlightedItems.forEach( function ( filterItem ) {
+                               if ( !filterItem.isSelected() ) {
+                                       // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
+                                       if ( this.findItemFromData( filterItem.getName() ) !== null ) {
+                                               this.removeTagByData( filterItem.getName() );
+                                       }
+                               }
+                       }.bind( this ) );
+               }
+
+               this.setSavedQueryVisibility();
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
+               var menuOption = this.menu.getItemFromModel( tagItem.getModel() );
+
+               this.menu.setUserSelecting( true );
+               // Parent method
+               FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
+
+               // Switch view
+               this.controller.resetSearchForView( tagItem.getView() );
+
+               this.selectTag( tagItem );
+               this.scrollToTop( menuOption.$element );
+
+               this.menu.setUserSelecting( false );
+       };
+
+       /**
+        * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
+        * If no items are given, reset selection from all.
+        *
+        * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
+        *  omit to deselect all
+        */
+       FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
+               var i, len, selected;
+
+               for ( i = 0, len = this.items.length; i < len; i++ ) {
+                       selected = this.items[ i ] === item;
+                       if ( this.items[ i ].isSelected() !== selected ) {
+                               this.items[ i ].toggleSelected( selected );
+                       }
+               }
+       };
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
+               // Parent method
+               FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
+
+               this.controller.clearFilter( tagItem.getName() );
+
+               tagItem.destroy();
+       };
+
+       /**
+        * Respond to click event on the reset button
+        */
+       FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
+               if ( this.model.areVisibleFiltersEmpty() ) {
+                       // Reset to default filters
+                       this.controller.resetToDefaults();
+               } else {
+                       // Reset to have no filters
+                       this.controller.emptyFilters();
+               }
+       };
+
+       /**
+        * Respond to hide/show button click
+        */
+       FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
+               this.toggleCollapsed();
+       };
+
+       /**
+        * Toggle the collapsed state of the filters widget
+        *
+        * @param {boolean} isCollapsed Widget is collapsed
+        */
+       FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
+               isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
+
+               this.collapsed = isCollapsed;
+
+               if ( isCollapsed ) {
+                       // If we are collapsing, close the menu, in case it was open
+                       // We should make sure the menu closes before the rest of the elements
+                       // are hidden, otherwise there is an unknown error in jQuery as ooui
+                       // sets and unsets properties on the input (which is hidden at that point)
+                       this.menu.toggle( false );
+               }
+               this.input.setDisabled( isCollapsed );
+               this.hideShowButton.setLabel( mw.msg(
+                       isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
+               ) );
+               this.hideShowButton.setTitle( mw.msg(
+                       isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
+               ) );
+
+               // Toggle the wrapper class, so we have min height values correctly throughout
+               this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );
+
+               // Save the state
+               this.controller.updateCollapsedState( isCollapsed );
+       };
+
+       /**
+        * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
+        */
+       FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
+               var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
+                       currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
+                       hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
+
+               this.resetButton.setIcon(
+                       currFiltersAreEmpty ? 'history' : 'trash'
+               );
+
+               this.resetButton.setLabel(
+                       currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
+               );
+               this.resetButton.setTitle(
+                       currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
+               );
+
+               this.resetButton.toggle( !hideResetButton );
+               this.emptyFilterMessage.toggle( currFiltersAreEmpty );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
+               return new MenuSelectWidget(
+                       this.controller,
+                       this.model,
+                       menuConfig
+               );
+       };
+
+       /**
+        * @inheritdoc
+        */
+       FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
+               var filterItem = this.model.getItemByName( data );
+
+               if ( filterItem ) {
+                       return new FilterTagItemWidget(
+                               this.controller,
+                               this.model,
+                               this.model.getInvertModel(),
+                               filterItem,
+                               {
+                                       $overlay: this.$overlay
+                               }
+                       );
+               }
+       };
+
+       FilterTagMultiselectWidget.prototype.emphasize = function () {
+               if (
+                       !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
+               ) {
+                       this.$handle
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
+
+                       setTimeout( function () {
+                               this.$handle
+                                       .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
+
+                               setTimeout( function () {
+                                       this.$handle
+                                               .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
+                               }.bind( this ), 1000 );
+                       }.bind( this ), 500 );
+
+               }
+       };
+       /**
+        * Scroll the element to top within its container
+        *
+        * @private
+        * @param {jQuery} $element Element to position
+        * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
+        *  much space (in pixels) above the widget.
+        * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
+        * @param {number} [threshold.min] Minimum distance above the element
+        * @param {number} [threshold.max] Minimum distance below the element
+        */
+       FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
+               var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
+                       pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
+                       containerScrollTop = $( container ).scrollTop(),
+                       effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
+                       newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
+
+               // Scroll to item
+               if (
+                       threshold === undefined ||
+                       (
+                               (
+                                       threshold.min === undefined ||
+                                       newScrollTop - containerScrollTop >= threshold.min
+                               ) &&
+                               (
+                                       threshold.max === undefined ||
+                                       newScrollTop - containerScrollTop <= threshold.max
+                               )
+                       )
+               ) {
+                       // eslint-disable-next-line jquery/no-animate
+                       $( container ).animate( {
+                               scrollTop: newScrollTop
+                       } );
+               }
+       };
+
+       module.exports = FilterTagMultiselectWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/FilterWrapperWidget.js
new file mode 100644 (file)
index 0000000..cb297f6
--- /dev/null
@@ -0,0 +1,139 @@
+( function () {
+       var FilterTagMultiselectWidget = require( './FilterTagMultiselectWidget.js' ),
+               LiveUpdateButtonWidget = require( './LiveUpdateButtonWidget.js' ),
+               ChangesLimitAndDateButtonWidget = require( './ChangesLimitAndDateButtonWidget.js' ),
+               FilterWrapperWidget;
+
+       /**
+        * List displaying all filter groups
+        *
+        * @class mw.rcfilters.ui.FilterWrapperWidget
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.PendingElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+        * @param {Object} [config] Configuration object
+        * @cfg {Object} [filters] A definition of the filter groups in this list
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
+        *  system. If not given, falls back to this widget's $element
+        * @cfg {boolean} [collapsed] Filter area is collapsed
+        */
+       FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
+               controller, model, savedQueriesModel, changesListModel, config
+       ) {
+               var $bottom;
+               config = config || {};
+
+               // Parent
+               FilterWrapperWidget.parent.call( this, config );
+               // Mixin constructors
+               OO.ui.mixin.PendingElement.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+               this.queriesModel = savedQueriesModel;
+               this.changesListModel = changesListModel;
+               this.$overlay = config.$overlay || this.$element;
+               this.$wrapper = config.$wrapper || this.$element;
+
+               this.filterTagWidget = new FilterTagMultiselectWidget(
+                       this.controller,
+                       this.model,
+                       this.queriesModel,
+                       {
+                               $overlay: this.$overlay,
+                               collapsed: config.collapsed,
+                               $wrapper: this.$wrapper
+                       }
+               );
+
+               this.liveUpdateButton = new LiveUpdateButtonWidget(
+                       this.controller,
+                       this.changesListModel
+               );
+
+               this.numChangesAndDateWidget = new ChangesLimitAndDateButtonWidget(
+                       this.controller,
+                       this.model,
+                       {
+                               $overlay: this.$overlay
+                       }
+               );
+
+               this.showNewChangesLink = new OO.ui.ButtonWidget( {
+                       icon: 'reload',
+                       framed: false,
+                       label: mw.msg( 'rcfilters-show-new-changes' ),
+                       flags: [ 'progressive' ],
+                       classes: [ 'mw-rcfilters-ui-filterWrapperWidget-showNewChanges' ]
+               } );
+
+               // Events
+               this.filterTagWidget.menu.connect( this, { toggle: [ 'emit', 'menuToggle' ] } );
+               this.changesListModel.connect( this, { newChangesExist: 'onNewChangesExist' } );
+               this.showNewChangesLink.connect( this, { click: 'onShowNewChangesClick' } );
+               this.showNewChangesLink.toggle( false );
+
+               // Initialize
+               this.$top = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
+
+               $bottom = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
+                       .append(
+                               this.showNewChangesLink.$element,
+                               this.numChangesAndDateWidget.$element
+                       );
+
+               if ( this.controller.pollingRate ) {
+                       $bottom.prepend( this.liveUpdateButton.$element );
+               }
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
+                       .append(
+                               this.$top,
+                               this.filterTagWidget.$element,
+                               $bottom
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FilterWrapperWidget, OO.ui.Widget );
+       OO.mixinClass( FilterWrapperWidget, OO.ui.mixin.PendingElement );
+
+       /* Methods */
+
+       /**
+        * Set the content of the top section
+        *
+        * @param {jQuery} $topSectionElement
+        */
+       FilterWrapperWidget.prototype.setTopSection = function ( $topSectionElement ) {
+               this.$top.append( $topSectionElement );
+       };
+
+       /**
+        * Respond to the user clicking the 'show new changes' button
+        */
+       FilterWrapperWidget.prototype.onShowNewChangesClick = function () {
+               this.controller.showNewChanges();
+       };
+
+       /**
+        * Respond to changes list model newChangesExist
+        *
+        * @param {boolean} newChangesExist Whether new changes exist
+        */
+       FilterWrapperWidget.prototype.onNewChangesExist = function ( newChangesExist ) {
+               this.showNewChangesLink.toggle( newChangesExist );
+       };
+
+       module.exports = FilterWrapperWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/FormWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/FormWrapperWidget.js
new file mode 100644 (file)
index 0000000..dbf1776
--- /dev/null
@@ -0,0 +1,176 @@
+( function () {
+       /**
+        * Wrapper for the RC form with hide/show links
+        * Must be constructed after the model is initialized.
+        *
+        * @class mw.rcfilters.ui.FormWrapperWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Changes list view model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changeListModel Changes list view model
+        * @param {mw.rcfilters.Controller} controller RCfilters controller
+        * @param {jQuery} $formRoot Root element of the form to attach to
+        * @param {Object} config Configuration object
+        */
+       var FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( filtersModel, changeListModel, controller, $formRoot, config ) {
+               config = config || {};
+
+               // Parent
+               FormWrapperWidget.parent.call( this, $.extend( {}, config, {
+                       $element: $formRoot
+               } ) );
+
+               this.changeListModel = changeListModel;
+               this.filtersModel = filtersModel;
+               this.controller = controller;
+               this.$submitButton = this.$element.find( 'form input[type=submit]' );
+
+               this.$element
+                       .on( 'click', 'a[data-params]', this.onLinkClick.bind( this ) );
+
+               this.$element
+                       .on( 'submit', 'form', this.onFormSubmit.bind( this ) );
+
+               // Events
+               this.changeListModel.connect( this, {
+                       invalidate: 'onChangesModelInvalidate',
+                       update: 'onChangesModelUpdate'
+               } );
+
+               // Initialize
+               this.cleanUpFieldset();
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-FormWrapperWidget' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( FormWrapperWidget, OO.ui.Widget );
+
+       /**
+        * Respond to link click
+        *
+        * @param {jQuery.Event} e Event
+        * @return {boolean} false
+        */
+       FormWrapperWidget.prototype.onLinkClick = function ( e ) {
+               this.controller.updateChangesList( $( e.target ).data( 'params' ) );
+               return false;
+       };
+
+       /**
+        * Respond to form submit event
+        *
+        * @param {jQuery.Event} e Event
+        * @return {boolean} false
+        */
+       FormWrapperWidget.prototype.onFormSubmit = function ( e ) {
+               var data = {};
+
+               // Collect all data from form
+               $( e.target ).find( 'input:not([type="hidden"],[type="submit"]), select' ).each( function () {
+                       var value = '';
+
+                       if ( !$( this ).is( ':checkbox' ) || $( this ).is( ':checked' ) ) {
+                               value = $( this ).val();
+                       }
+
+                       data[ $( this ).prop( 'name' ) ] = value;
+               } );
+
+               this.controller.updateChangesList( data );
+               return false;
+       };
+
+       /**
+        * Respond to model invalidate
+        */
+       FormWrapperWidget.prototype.onChangesModelInvalidate = function () {
+               this.$submitButton.prop( 'disabled', true );
+       };
+
+       /**
+        * Respond to model update, replace the show/hide links with the ones from the
+        * server so they feature the correct state.
+        *
+        * @param {jQuery|string} $changesList Updated changes list
+        * @param {jQuery} $fieldset Updated fieldset
+        * @param {string} noResultsDetails Type of no result error
+        * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
+        */
+       FormWrapperWidget.prototype.onChangesModelUpdate = function ( $changesList, $fieldset, noResultsDetails, isInitialDOM ) {
+               this.$submitButton.prop( 'disabled', false );
+
+               // Replace the entire fieldset
+               this.$element.empty().append( $fieldset.contents() );
+
+               if ( !isInitialDOM ) {
+                       // Make sure enhanced RC re-initializes correctly
+                       mw.hook( 'wikipage.content' ).fire( this.$element );
+               }
+
+               this.cleanUpFieldset();
+       };
+
+       /**
+        * Clean up the old-style show/hide that we have implemented in the filter list
+        */
+       FormWrapperWidget.prototype.cleanUpFieldset = function () {
+               this.$element.find( '.clshowhideoption[data-feature-in-structured-ui=1]' ).each( function () {
+                       // HACK: Remove the text node after the span.
+                       // If there isn't one, we're at the end, so remove the text node before the span.
+                       // This would be unnecessary if we added separators with CSS.
+                       if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
+                               this.parentNode.removeChild( this.nextSibling );
+                       } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
+                               this.parentNode.removeChild( this.previousSibling );
+                       }
+                       // Remove the span itself
+                       this.parentNode.removeChild( this );
+               } );
+
+               // Hide namespaces and tags
+               this.$element.find( '.namespaceForm' ).detach();
+               this.$element.find( '.mw-tagfilter-label' ).closest( 'tr' ).detach();
+
+               // Hide Related Changes page name form
+               this.$element.find( '.targetForm' ).detach();
+
+               // misc: limit, days, watchlist info msg
+               this.$element.find( '.rclinks, .cldays, .wlinfo' ).detach();
+
+               if ( !this.$element.find( '.mw-recentchanges-table tr' ).length ) {
+                       this.$element.find( '.mw-recentchanges-table' ).detach();
+                       this.$element.find( 'hr' ).detach();
+               }
+
+               // Get rid of all <br>s, which are inside rcshowhide
+               // If we still have content in rcshowhide, the <br>s are
+               // gone. Instead, the CSS now has a rule to mark all <span>s
+               // inside .rcshowhide with display:block; to simulate newlines
+               // where they're actually needed.
+               this.$element.find( 'br' ).detach();
+               if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
+                       this.$element.find( '.rcshowhide' ).detach();
+               }
+
+               if ( this.$element.find( '.cloption' ).text().trim() === '' ) {
+                       this.$element.find( '.cloption-submit' ).detach();
+               }
+
+               this.$element.find(
+                       '.rclistfrom, .rcnotefrom, .rcoptions-listfromreset'
+               ).detach();
+
+               // Get rid of the legend
+               this.$element.find( 'legend' ).detach();
+
+               // Check if the element is essentially empty, and detach it if it is
+               if ( !this.$element.text().trim().length ) {
+                       this.$element.detach();
+               }
+       };
+
+       module.exports = FormWrapperWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/GroupWidget.js b/resources/src/mediawiki.rcfilters/ui/GroupWidget.js
new file mode 100644 (file)
index 0000000..17c038e
--- /dev/null
@@ -0,0 +1,45 @@
+( function () {
+       /**
+        * A group widget to allow for aggregation of events
+        *
+        * @class mw.rcfilters.ui.GroupWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {Object} [config] Configuration object
+        * @param {Object} [events] Events to aggregate. The object represent the
+        *  event name to aggregate and the event value to emit on aggregate for items.
+        */
+       var GroupWidget = function MwRcfiltersUiViewSwitchWidget( config ) {
+               var aggregate = {};
+
+               config = config || {};
+
+               // Parent constructor
+               GroupWidget.parent.call( this, config );
+
+               // Mixin constructors
+               OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
+
+               if ( config.events ) {
+                       // Aggregate events
+                       // eslint-disable-next-line jquery/no-each-util
+                       $.each( config.events, function ( eventName, eventEmit ) {
+                               aggregate[ eventName ] = eventEmit;
+                       } );
+
+                       this.aggregate( aggregate );
+               }
+
+               if ( Array.isArray( config.items ) ) {
+                       this.addItems( config.items );
+               }
+       };
+
+       /* Initialize */
+
+       OO.inheritClass( GroupWidget, OO.ui.Widget );
+       OO.mixinClass( GroupWidget, OO.ui.mixin.GroupWidget );
+
+       module.exports = GroupWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/HighlightColorPickerWidget.js b/resources/src/mediawiki.rcfilters/ui/HighlightColorPickerWidget.js
new file mode 100644 (file)
index 0000000..cb5f8eb
--- /dev/null
@@ -0,0 +1,125 @@
+( function () {
+       /**
+        * A widget representing a filter item highlight color picker
+        *
+        * @class mw.rcfilters.ui.HighlightColorPickerWidget
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.LabelElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {Object} [config] Configuration object
+        */
+       var HighlightColorPickerWidget = function MwRcfiltersUiHighlightColorPickerWidget( controller, config ) {
+               var colors = [ 'none' ].concat( mw.rcfilters.HighlightColors );
+               config = config || {};
+
+               // Parent
+               HighlightColorPickerWidget.parent.call( this, config );
+               // Mixin constructors
+               OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+                       label: mw.message( 'rcfilters-highlightmenu-title' ).text()
+               } ) );
+
+               this.controller = controller;
+
+               this.currentSelection = 'none';
+               this.buttonSelect = new OO.ui.ButtonSelectWidget( {
+                       items: colors.map( function ( color ) {
+                               return new OO.ui.ButtonOptionWidget( {
+                                       icon: color === 'none' ? 'check' : null,
+                                       data: color,
+                                       classes: [
+                                               'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color',
+                                               'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color-' + color
+                                       ],
+                                       framed: false
+                               } );
+                       } ),
+                       classes: 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect'
+               } );
+
+               // Event
+               this.buttonSelect.connect( this, { choose: 'onChooseColor' } );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget' )
+                       .append(
+                               this.$label
+                                       .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget-label' ),
+                               this.buttonSelect.$element
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( HighlightColorPickerWidget, OO.ui.Widget );
+       OO.mixinClass( HighlightColorPickerWidget, OO.ui.mixin.LabelElement );
+
+       /* Events */
+
+       /**
+        * @event chooseColor
+        * @param {string} The chosen color
+        *
+        * A color has been chosen
+        */
+
+       /* Methods */
+
+       /**
+        * Bind the color picker to an item
+        * @param {mw.rcfilters.dm.FilterItem} filterItem
+        */
+       HighlightColorPickerWidget.prototype.setFilterItem = function ( filterItem ) {
+               if ( this.filterItem ) {
+                       this.filterItem.disconnect( this );
+               }
+
+               this.filterItem = filterItem;
+               this.filterItem.connect( this, { update: 'updateUiBasedOnModel' } );
+               this.updateUiBasedOnModel();
+       };
+
+       /**
+        * Respond to item model update event
+        */
+       HighlightColorPickerWidget.prototype.updateUiBasedOnModel = function () {
+               this.selectColor( this.filterItem.getHighlightColor() || 'none' );
+       };
+
+       /**
+        * Select the color for this widget
+        *
+        * @param {string} color Selected color
+        */
+       HighlightColorPickerWidget.prototype.selectColor = function ( color ) {
+               var previousItem = this.buttonSelect.findItemFromData( this.currentSelection ),
+                       selectedItem = this.buttonSelect.findItemFromData( color );
+
+               if ( this.currentSelection !== color ) {
+                       this.currentSelection = color;
+
+                       this.buttonSelect.selectItem( selectedItem );
+                       if ( previousItem ) {
+                               previousItem.setIcon( null );
+                       }
+
+                       if ( selectedItem ) {
+                               selectedItem.setIcon( 'check' );
+                       }
+               }
+       };
+
+       HighlightColorPickerWidget.prototype.onChooseColor = function ( button ) {
+               var color = button.data;
+               if ( color === 'none' ) {
+                       this.controller.clearHighlightColor( this.filterItem.getName() );
+               } else {
+                       this.controller.setHighlightColor( this.filterItem.getName(), color );
+               }
+               this.emit( 'chooseColor', color );
+       };
+
+       module.exports = HighlightColorPickerWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/HighlightPopupWidget.js b/resources/src/mediawiki.rcfilters/ui/HighlightPopupWidget.js
new file mode 100644 (file)
index 0000000..4c467df
--- /dev/null
@@ -0,0 +1,68 @@
+( function () {
+       var HighlightColorPickerWidget = require( './HighlightColorPickerWidget.js' ),
+               HighlightPopupWidget;
+       /**
+        * A popup containing a color picker, for setting highlight colors.
+        *
+        * @class mw.rcfilters.ui.HighlightPopupWidget
+        * @extends OO.ui.PopupWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {Object} [config] Configuration object
+        */
+       HighlightPopupWidget = function MwRcfiltersUiHighlightPopupWidget( controller, config ) {
+               config = config || {};
+
+               // Parent
+               HighlightPopupWidget.parent.call( this, $.extend( {
+                       autoClose: true,
+                       anchor: false,
+                       padded: true,
+                       align: 'backwards',
+                       horizontalPosition: 'end',
+                       width: 290
+               }, config ) );
+
+               this.colorPicker = new HighlightColorPickerWidget( controller );
+
+               this.colorPicker.connect( this, { chooseColor: 'onChooseColor' } );
+
+               this.$body.append( this.colorPicker.$element );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( HighlightPopupWidget, OO.ui.PopupWidget );
+
+       /* Methods */
+
+       /**
+        * Set the button (or other widget) that this popup should hang off.
+        *
+        * @param {OO.ui.Widget} widget Widget the popup should orient itself to
+        */
+       HighlightPopupWidget.prototype.setAssociatedButton = function ( widget ) {
+               this.setFloatableContainer( widget.$element );
+               this.$autoCloseIgnore = widget.$element;
+       };
+
+       /**
+        * Set the filter item that this popup should control the highlight color for.
+        *
+        * @param {mw.rcfilters.dm.FilterItem} item
+        */
+       HighlightPopupWidget.prototype.setFilterItem = function ( item ) {
+               this.colorPicker.setFilterItem( item );
+       };
+
+       /**
+        * When the user chooses a color in the color picker, close the popup.
+        */
+       HighlightPopupWidget.prototype.onChooseColor = function () {
+               this.toggle( false );
+       };
+
+       module.exports = HighlightPopupWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/ItemMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/ItemMenuOptionWidget.js
new file mode 100644 (file)
index 0000000..56ed628
--- /dev/null
@@ -0,0 +1,172 @@
+( function () {
+       var FilterItemHighlightButton = require( './FilterItemHighlightButton.js' ),
+               CheckboxInputWidget = require( './CheckboxInputWidget.js' ),
+               ItemMenuOptionWidget;
+
+       /**
+        * A widget representing a base toggle item
+        *
+        * @class mw.rcfilters.ui.ItemMenuOptionWidget
+        * @extends OO.ui.MenuOptionWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+        * @param {mw.rcfilters.dm.ItemModel} invertModel
+        * @param {mw.rcfilters.dm.ItemModel} itemModel Item model
+        * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
+        * @param {Object} config Configuration object
+        */
+       ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget(
+               controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
+       ) {
+               var layout,
+                       classes = [],
+                       $label = $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.filtersViewModel = filtersViewModel;
+               this.invertModel = invertModel;
+               this.itemModel = itemModel;
+
+               // Parent
+               ItemMenuOptionWidget.parent.call( this, $.extend( {
+                       // Override the 'check' icon that OOUI defines
+                       icon: '',
+                       data: this.itemModel.getName(),
+                       label: this.itemModel.getLabel()
+               }, config ) );
+
+               this.checkboxWidget = new CheckboxInputWidget( {
+                       value: this.itemModel.getName(),
+                       selected: this.itemModel.isSelected()
+               } );
+
+               $label.append(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-title' )
+                               .append( $( '<bdi>' ).append( this.$label ) )
+               );
+               if ( this.itemModel.getDescription() ) {
+                       $label.append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-desc' )
+                                       .append( $( '<bdi>' ).text( this.itemModel.getDescription() ) )
+                       );
+               }
+
+               this.highlightButton = new FilterItemHighlightButton(
+                       this.controller,
+                       this.itemModel,
+                       highlightPopup,
+                       {
+                               $overlay: config.$overlay || this.$element,
+                               title: mw.msg( 'rcfilters-highlightmenu-help' )
+                       }
+               );
+               this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
+
+               this.excludeLabel = new OO.ui.LabelWidget( {
+                       label: mw.msg( 'rcfilters-filter-excluded' )
+               } );
+               this.excludeLabel.toggle(
+                       this.itemModel.getGroupModel().getView() === 'namespaces' &&
+                       this.itemModel.isSelected() &&
+                       this.invertModel.isSelected()
+               );
+
+               layout = new OO.ui.FieldLayout( this.checkboxWidget, {
+                       label: $label,
+                       align: 'inline'
+               } );
+
+               // Events
+               this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
+               this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
+               this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
+               // HACK: Prevent defaults on 'click' for the label so it
+               // doesn't steal the focus away from the input. This means
+               // we can continue arrow-movement after we click the label
+               // and is consistent with the checkbox *itself* also preventing
+               // defaults on 'click' as well.
+               layout.$label.on( 'click', false );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
+                       .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
+                                                                       .append( layout.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
+                                                                       .append( this.excludeLabel.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
+                                                                       .append( this.highlightButton.$element )
+                                                       )
+                                       )
+                       );
+
+               if ( this.itemModel.getIdentifiers() ) {
+                       this.itemModel.getIdentifiers().forEach( function ( ident ) {
+                               classes.push( 'mw-rcfilters-ui-itemMenuOptionWidget-identifier-' + ident );
+                       } );
+
+                       this.$element.addClass( classes );
+               }
+
+               this.updateUiBasedOnState();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( ItemMenuOptionWidget, OO.ui.MenuOptionWidget );
+
+       /* Static properties */
+
+       // We do our own scrolling to top
+       ItemMenuOptionWidget.static.scrollIntoViewOnSelect = false;
+
+       /* Methods */
+
+       /**
+        * Respond to item model update event
+        */
+       ItemMenuOptionWidget.prototype.updateUiBasedOnState = function () {
+               this.checkboxWidget.setSelected( this.itemModel.isSelected() );
+
+               this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
+               this.excludeLabel.toggle(
+                       this.itemModel.getGroupModel().getView() === 'namespaces' &&
+                       this.itemModel.isSelected() &&
+                       this.invertModel.isSelected()
+               );
+               this.toggle( this.itemModel.isVisible() );
+       };
+
+       /**
+        * Get the name of this filter
+        *
+        * @return {string} Filter name
+        */
+       ItemMenuOptionWidget.prototype.getName = function () {
+               return this.itemModel.getName();
+       };
+
+       ItemMenuOptionWidget.prototype.getModel = function () {
+               return this.itemModel;
+       };
+
+       module.exports = ItemMenuOptionWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/LiveUpdateButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/LiveUpdateButtonWidget.js
new file mode 100644 (file)
index 0000000..3ccb6e2
--- /dev/null
@@ -0,0 +1,72 @@
+( function () {
+       /**
+        * Widget for toggling live updates
+        *
+        * @class mw.rcfilters.ui.LiveUpdateButtonWidget
+        * @extends OO.ui.ToggleButtonWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+        * @param {Object} [config] Configuration object
+        */
+       var LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, changesListModel, config ) {
+               config = config || {};
+
+               // Parent
+               LiveUpdateButtonWidget.parent.call( this, $.extend( {
+                       label: mw.message( 'rcfilters-liveupdates-button' ).text()
+               }, config ) );
+
+               this.controller = controller;
+               this.model = changesListModel;
+
+               // Events
+               this.connect( this, { click: 'onClick' } );
+               this.model.connect( this, { liveUpdateChange: 'onLiveUpdateChange' } );
+
+               this.$element.addClass( 'mw-rcfilters-ui-liveUpdateButtonWidget' );
+
+               this.setState( false );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( LiveUpdateButtonWidget, OO.ui.ToggleButtonWidget );
+
+       /* Methods */
+
+       /**
+        * Respond to the button being clicked
+        */
+       LiveUpdateButtonWidget.prototype.onClick = function () {
+               this.controller.toggleLiveUpdate();
+       };
+
+       /**
+        * Set the button's state and change its appearance
+        *
+        * @param {boolean} enable Whether the 'live update' feature is now on/off
+        */
+       LiveUpdateButtonWidget.prototype.setState = function ( enable ) {
+               this.setValue( enable );
+               this.setIcon( enable ? 'stop' : 'play' );
+               this.setTitle( mw.message(
+                       enable ?
+                               'rcfilters-liveupdates-button-title-on' :
+                               'rcfilters-liveupdates-button-title-off'
+               ).text() );
+       };
+
+       /**
+        * Respond to the 'live update' feature being turned on/off
+        *
+        * @param {boolean} enable Whether the 'live update' feature is now on/off
+        */
+       LiveUpdateButtonWidget.prototype.onLiveUpdateChange = function ( enable ) {
+               this.setState( enable );
+       };
+
+       module.exports = LiveUpdateButtonWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/MainWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/MainWrapperWidget.js
new file mode 100644 (file)
index 0000000..bc1cac8
--- /dev/null
@@ -0,0 +1,142 @@
+( function () {
+       var SavedLinksListWidget = require( './SavedLinksListWidget.js' ),
+               FilterWrapperWidget = require( './FilterWrapperWidget.js' ),
+               ChangesListWrapperWidget = require( './ChangesListWrapperWidget.js' ),
+               RcTopSectionWidget = require( './RcTopSectionWidget.js' ),
+               RclTopSectionWidget = require( './RclTopSectionWidget.js' ),
+               WatchlistTopSectionWidget = require( './WatchlistTopSectionWidget.js' ),
+               FormWrapperWidget = require( './FormWrapperWidget.js' ),
+               MainWrapperWidget;
+
+       /**
+        * Wrapper for changes list content
+        *
+        * @class mw.rcfilters.ui.MainWrapperWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} $topSection Top section container
+        * @cfg {jQuery} $filtersContainer
+        * @cfg {jQuery} $changesListContainer
+        * @cfg {jQuery} $formContainer
+        * @cfg {boolean} [collapsed] Filter area is collapsed
+        * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
+        *  system. If not given, falls back to this widget's $element
+        */
+       MainWrapperWidget = function MwRcfiltersUiMainWrapperWidget(
+               controller, model, savedQueriesModel, changesListModel, config
+       ) {
+               config = $.extend( {}, config );
+
+               // Parent
+               MainWrapperWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+               this.changesListModel = changesListModel;
+               this.$topSection = config.$topSection;
+               this.$filtersContainer = config.$filtersContainer;
+               this.$changesListContainer = config.$changesListContainer;
+               this.$formContainer = config.$formContainer;
+               this.$overlay = $( '<div>' ).addClass( 'mw-rcfilters-ui-overlay' );
+               this.$wrapper = config.$wrapper || this.$element;
+
+               this.savedLinksListWidget = new SavedLinksListWidget(
+                       controller, savedQueriesModel, { $overlay: this.$overlay }
+               );
+
+               this.filtersWidget = new FilterWrapperWidget(
+                       controller,
+                       model,
+                       savedQueriesModel,
+                       changesListModel,
+                       {
+                               $overlay: this.$overlay,
+                               $wrapper: this.$wrapper,
+                               collapsed: config.collapsed
+                       }
+               );
+
+               this.changesListWidget = new ChangesListWrapperWidget(
+                       model, changesListModel, controller, this.$changesListContainer );
+
+               /* Events */
+
+               // Toggle changes list overlay when filters menu opens/closes. We use overlay on changes list
+               // to prevent users from accidentally clicking on links in results, while menu is opened.
+               // Overlay on changes list is not the same as this.$overlay
+               this.filtersWidget.connect( this, { menuToggle: this.onFilterMenuToggle.bind( this ) } );
+
+               // Initialize
+               this.$filtersContainer.append( this.filtersWidget.$element );
+               $( 'body' )
+                       .append( this.$overlay )
+                       .addClass( 'mw-rcfilters-ui-initialized' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( MainWrapperWidget, OO.ui.Widget );
+
+       /* Methods */
+
+       /**
+        * Set the content of the top section, depending on the type of special page.
+        *
+        * @param {string} specialPage
+        */
+       MainWrapperWidget.prototype.setTopSection = function ( specialPage ) {
+               var topSection;
+
+               if ( specialPage === 'Recentchanges' ) {
+                       topSection = new RcTopSectionWidget(
+                               this.savedLinksListWidget, this.$topSection
+                       );
+                       this.filtersWidget.setTopSection( topSection.$element );
+               }
+
+               if ( specialPage === 'Recentchangeslinked' ) {
+                       topSection = new RclTopSectionWidget(
+                               this.savedLinksListWidget, this.controller,
+                               this.model.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' ),
+                               this.model.getGroup( 'page' ).getItemByParamName( 'target' )
+                       );
+
+                       this.filtersWidget.setTopSection( topSection.$element );
+               }
+
+               if ( specialPage === 'Watchlist' ) {
+                       topSection = new WatchlistTopSectionWidget(
+                               this.controller, this.changesListModel, this.savedLinksListWidget, this.$topSection
+                       );
+
+                       this.filtersWidget.setTopSection( topSection.$element );
+               }
+       };
+
+       /**
+        * Filter menu toggle event listener
+        *
+        * @param {boolean} isVisible
+        */
+       MainWrapperWidget.prototype.onFilterMenuToggle = function ( isVisible ) {
+               this.changesListWidget.toggleOverlay( isVisible );
+       };
+
+       /**
+        * Initialize FormWrapperWidget
+        *
+        * @return {mw.rcfilters.ui.FormWrapperWidget} Form wrapper widget
+        */
+       MainWrapperWidget.prototype.initFormWidget = function () {
+               return new FormWrapperWidget(
+                       this.model, this.changesListModel, this.controller, this.$formContainer );
+       };
+
+       module.exports = MainWrapperWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/MarkSeenButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/MarkSeenButtonWidget.js
new file mode 100644 (file)
index 0000000..3914337
--- /dev/null
@@ -0,0 +1,58 @@
+( function () {
+       /**
+        * Button for marking all changes as seen on the Watchlist
+        *
+        * @class mw.rcfilters.ui.MarkSeenButtonWidget
+        * @extends OO.ui.ButtonWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
+        * @param {Object} [config] Configuration object
+        */
+       var MarkSeenButtonWidget = function MwRcfiltersUiMarkSeenButtonWidget( controller, model, config ) {
+               config = config || {};
+
+               // Parent
+               MarkSeenButtonWidget.parent.call( this, $.extend( {
+                       label: mw.message( 'rcfilters-watchlist-markseen-button' ).text(),
+                       icon: 'checkAll'
+               }, config ) );
+
+               this.controller = controller;
+               this.model = model;
+
+               // Events
+               this.connect( this, { click: 'onClick' } );
+               this.model.connect( this, { update: 'onModelUpdate' } );
+
+               this.$element.addClass( 'mw-rcfilters-ui-markSeenButtonWidget' );
+
+               this.onModelUpdate();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( MarkSeenButtonWidget, OO.ui.ButtonWidget );
+
+       /* Methods */
+
+       /**
+        * Respond to the button being clicked
+        */
+       MarkSeenButtonWidget.prototype.onClick = function () {
+               this.controller.markAllChangesAsSeen();
+               // assume there's no more unseen changes until the next model update
+               this.setDisabled( true );
+       };
+
+       /**
+        * Respond to the model being updated with new changes
+        */
+       MarkSeenButtonWidget.prototype.onModelUpdate = function () {
+               this.setDisabled( !this.model.hasUnseenWatchedChanges() );
+       };
+
+       module.exports = MarkSeenButtonWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/MenuSelectWidget.js b/resources/src/mediawiki.rcfilters/ui/MenuSelectWidget.js
new file mode 100644 (file)
index 0000000..c352f5a
--- /dev/null
@@ -0,0 +1,368 @@
+( function () {
+       var FilterMenuHeaderWidget = require( './FilterMenuHeaderWidget.js' ),
+               HighlightPopupWidget = require( './HighlightPopupWidget.js' ),
+               FilterMenuSectionOptionWidget = require( './FilterMenuSectionOptionWidget.js' ),
+               FilterMenuOptionWidget = require( './FilterMenuOptionWidget.js' ),
+               MenuSelectWidget;
+
+       /**
+        * A floating menu widget for the filter list
+        *
+        * @class mw.rcfilters.ui.MenuSelectWidget
+        * @extends OO.ui.MenuSelectWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        * @cfg {Object[]} [footers] An array of objects defining the footers for
+        *  this menu, with a definition whether they appear per specific views.
+        *  The expected structure is:
+        *  [
+        *     {
+        *        name: {string} A unique name for the footer object
+        *        $element: {jQuery} A jQuery object for the content of the footer
+        *        views: {string[]} Optional. An array stating which views this footer is
+        *               active on. Use null or omit to display this on all views.
+        *     }
+        *  ]
+        */
+       MenuSelectWidget = function MwRcfiltersUiMenuSelectWidget( controller, model, config ) {
+               var header;
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+               this.currentView = '';
+               this.views = {};
+               this.userSelecting = false;
+
+               this.menuInitialized = false;
+               this.$overlay = config.$overlay || this.$element;
+               this.$body = $( '<div>' ).addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
+               this.footers = [];
+
+               // Parent
+               MenuSelectWidget.parent.call( this, $.extend( config, {
+                       $autoCloseIgnore: this.$overlay,
+                       width: 650,
+                       // Our filtering is done through the model
+                       filterFromInput: false
+               } ) );
+               this.setGroupElement(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
+               );
+               this.setClippableElement( this.$body );
+               this.setClippableContainer( this.$element );
+
+               header = new FilterMenuHeaderWidget(
+                       this.controller,
+                       this.model,
+                       {
+                               $overlay: this.$overlay
+                       }
+               );
+
+               this.noResults = new OO.ui.LabelWidget( {
+                       label: mw.msg( 'rcfilters-filterlist-noresults' ),
+                       classes: [ 'mw-rcfilters-ui-menuSelectWidget-noresults' ]
+               } );
+
+               // Events
+               this.model.connect( this, {
+                       initialize: 'onModelInitialize',
+                       searchChange: 'onModelSearchChange'
+               } );
+
+               // Initialization
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-menuSelectWidget' )
+                       .append( header.$element )
+                       .append(
+                               this.$body
+                                       .append( this.$group, this.noResults.$element )
+                       );
+
+               // Append all footers; we will control their visibility
+               // based on view
+               config.footers = config.footers || [];
+               config.footers.forEach( function ( footerData ) {
+                       var isSticky = footerData.sticky === undefined ? true : !!footerData.sticky,
+                               adjustedData = {
+                                       // Wrap the element with our own footer wrapper
+                                       $element: $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer' )
+                                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer-' + footerData.name )
+                                               .append( footerData.$element ),
+                                       views: footerData.views
+                               };
+
+                       if ( !footerData.disabled ) {
+                               this.footers.push( adjustedData );
+
+                               if ( isSticky ) {
+                                       this.$element.append( adjustedData.$element );
+                               } else {
+                                       this.$body.append( adjustedData.$element );
+                               }
+                       }
+               }.bind( this ) );
+
+               // Switch to the correct view
+               this.updateView();
+       };
+
+       /* Initialize */
+
+       OO.inheritClass( MenuSelectWidget, OO.ui.MenuSelectWidget );
+
+       /* Events */
+
+       /* Methods */
+       MenuSelectWidget.prototype.onModelSearchChange = function () {
+               this.updateView();
+       };
+
+       /**
+        * @inheritdoc
+        */
+       MenuSelectWidget.prototype.toggle = function ( show ) {
+               this.lazyMenuCreation();
+               MenuSelectWidget.parent.prototype.toggle.call( this, show );
+               // Always open this menu downwards. FilterTagMultiselectWidget scrolls it into view.
+               this.setVerticalPosition( 'below' );
+       };
+
+       /**
+        * lazy creation of the menu
+        */
+       MenuSelectWidget.prototype.lazyMenuCreation = function () {
+               var widget = this,
+                       items = [],
+                       viewGroupCount = {},
+                       groups = this.model.getFilterGroups();
+
+               if ( this.menuInitialized ) {
+                       return;
+               }
+
+               this.menuInitialized = true;
+
+               // Create shared popup for highlight buttons
+               this.highlightPopup = new HighlightPopupWidget( this.controller );
+               this.$overlay.append( this.highlightPopup.$element );
+
+               // Count groups per view
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( groups, function ( groupName, groupModel ) {
+                       if ( !groupModel.isHidden() ) {
+                               viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
+                               viewGroupCount[ groupModel.getView() ]++;
+                       }
+               } );
+
+               // eslint-disable-next-line jquery/no-each-util
+               $.each( groups, function ( groupName, groupModel ) {
+                       var currentItems = [],
+                               view = groupModel.getView();
+
+                       if ( !groupModel.isHidden() ) {
+                               if ( viewGroupCount[ view ] > 1 ) {
+                                       // Only add a section header if there is more than
+                                       // one group
+                                       currentItems.push(
+                                               // Group section
+                                               new FilterMenuSectionOptionWidget(
+                                                       widget.controller,
+                                                       groupModel,
+                                                       {
+                                                               $overlay: widget.$overlay
+                                                       }
+                                               )
+                                       );
+                               }
+
+                               // Add items
+                               widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
+                                       currentItems.push(
+                                               new FilterMenuOptionWidget(
+                                                       widget.controller,
+                                                       widget.model,
+                                                       widget.model.getInvertModel(),
+                                                       filterItem,
+                                                       widget.highlightPopup,
+                                                       {
+                                                               $overlay: widget.$overlay
+                                                       }
+                                               )
+                                       );
+                               } );
+
+                               // Cache the items per view, so we can switch between them
+                               // without rebuilding the widgets each time
+                               widget.views[ view ] = widget.views[ view ] || [];
+                               widget.views[ view ] = widget.views[ view ].concat( currentItems );
+                               items = items.concat( currentItems );
+                       }
+               } );
+
+               this.addItems( items );
+               this.updateView();
+       };
+
+       /**
+        * Respond to model initialize event. Populate the menu from the model
+        */
+       MenuSelectWidget.prototype.onModelInitialize = function () {
+               this.menuInitialized = false;
+               // Set timeout for the menu to lazy build.
+               setTimeout( this.lazyMenuCreation.bind( this ) );
+       };
+
+       /**
+        * Update view
+        */
+       MenuSelectWidget.prototype.updateView = function () {
+               var viewName = this.model.getCurrentView();
+
+               if ( this.views[ viewName ] && this.currentView !== viewName ) {
+                       this.updateFooterVisibility( viewName );
+
+                       this.$element
+                               .data( 'view', viewName )
+                               .removeClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + this.currentView )
+                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + viewName );
+
+                       this.currentView = viewName;
+                       this.scrollToTop();
+               }
+
+               this.postProcessItems();
+               this.clip();
+       };
+
+       /**
+        * Go over the available footers and decide which should be visible
+        * for this view
+        *
+        * @param {string} [currentView] Current view
+        */
+       MenuSelectWidget.prototype.updateFooterVisibility = function ( currentView ) {
+               currentView = currentView || this.model.getCurrentView();
+
+               this.footers.forEach( function ( data ) {
+                       data.$element.toggle(
+                               // This footer should only be shown if it is configured
+                               // for all views or for this specific view
+                               !data.views || data.views.length === 0 || data.views.indexOf( currentView ) > -1
+                       );
+               } );
+       };
+
+       /**
+        * Post-process items after the visibility changed. Make sure
+        * that we always have an item selected, and that the no-results
+        * widget appears if the menu is empty.
+        */
+       MenuSelectWidget.prototype.postProcessItems = function () {
+               var i,
+                       itemWasSelected = false,
+                       items = this.getItems();
+
+               // If we are not already selecting an item, always make sure
+               // that the top item is selected
+               if ( !this.userSelecting ) {
+                       // Select the first item in the list
+                       for ( i = 0; i < items.length; i++ ) {
+                               if (
+                                       !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
+                                       items[ i ].isVisible()
+                               ) {
+                                       itemWasSelected = true;
+                                       this.selectItem( items[ i ] );
+                                       break;
+                               }
+                       }
+
+                       if ( !itemWasSelected ) {
+                               this.selectItem( null );
+                       }
+               }
+
+               this.noResults.toggle( !this.getItems().some( function ( item ) {
+                       return item.isVisible();
+               } ) );
+       };
+
+       /**
+        * Get the option widget that matches the model given
+        *
+        * @param {mw.rcfilters.dm.ItemModel} model Item model
+        * @return {mw.rcfilters.ui.ItemMenuOptionWidget} Option widget
+        */
+       MenuSelectWidget.prototype.getItemFromModel = function ( model ) {
+               this.lazyMenuCreation();
+               return this.views[ model.getGroupModel().getView() ].filter( function ( item ) {
+                       return item.getName() === model.getName();
+               } )[ 0 ];
+       };
+
+       /**
+        * @inheritdoc
+        */
+       MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
+               var nextItem,
+                       currentItem = this.findHighlightedItem() || this.findSelectedItem();
+
+               // Call parent
+               MenuSelectWidget.parent.prototype.onDocumentKeyDown.call( this, e );
+
+               // We want to select the item on arrow movement
+               // rather than just highlight it, like the menu
+               // does by default
+               if ( !this.isDisabled() && this.isVisible() ) {
+                       switch ( e.keyCode ) {
+                               case OO.ui.Keys.UP:
+                               case OO.ui.Keys.LEFT:
+                                       // Get the next item
+                                       nextItem = this.findRelativeSelectableItem( currentItem, -1 );
+                                       break;
+                               case OO.ui.Keys.DOWN:
+                               case OO.ui.Keys.RIGHT:
+                                       // Get the next item
+                                       nextItem = this.findRelativeSelectableItem( currentItem, 1 );
+                                       break;
+                       }
+
+                       nextItem = nextItem && nextItem.constructor.static.selectable ?
+                               nextItem : null;
+
+                       // Select the next item
+                       this.selectItem( nextItem );
+               }
+       };
+
+       /**
+        * Scroll to the top of the menu
+        */
+       MenuSelectWidget.prototype.scrollToTop = function () {
+               this.$body.scrollTop( 0 );
+       };
+
+       /**
+        * Set whether the user is currently selecting an item.
+        * This is important when the user selects an item that is in between
+        * different views, and makes sure we do not re-select a different
+        * item (like the item on top) when this is happening.
+        *
+        * @param {boolean} isSelecting User is selecting
+        */
+       MenuSelectWidget.prototype.setUserSelecting = function ( isSelecting ) {
+               this.userSelecting = !!isSelecting;
+       };
+
+       module.exports = MenuSelectWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/RcTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/RcTopSectionWidget.js
new file mode 100644 (file)
index 0000000..6de9c40
--- /dev/null
@@ -0,0 +1,116 @@
+( function () {
+       /**
+        * Top section (between page title and filters) on Special:Recentchanges
+        *
+        * @class mw.rcfilters.ui.RcTopSectionWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+        * @param {jQuery} $topLinks Content of the community-defined links
+        * @param {Object} [config] Configuration object
+        */
+       var RcTopSectionWidget = function MwRcfiltersUiRcTopSectionWidget(
+               savedLinksListWidget, $topLinks, config
+       ) {
+               var toplinksTitle,
+                       topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
+                       topLinksCookie = mw.cookie.get( topLinksCookieName ),
+                       topLinksCookieValue = topLinksCookie || 'collapsed',
+                       widget = this;
+
+               config = config || {};
+
+               // Parent
+               RcTopSectionWidget.parent.call( this, config );
+
+               this.$topLinks = $topLinks;
+
+               toplinksTitle = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
+                       flags: [ 'progressive' ],
+                       label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
+               } );
+
+               this.$topLinks
+                       .makeCollapsible( {
+                               collapsed: topLinksCookieValue === 'collapsed',
+                               $customTogglers: toplinksTitle.$element
+                       } )
+                       .on( 'beforeExpand.mw-collapsible', function () {
+                               mw.cookie.set( topLinksCookieName, 'expanded' );
+                               toplinksTitle.setIndicator( 'up' );
+                               widget.switchTopLinks( 'expanded' );
+                       } )
+                       .on( 'beforeCollapse.mw-collapsible', function () {
+                               mw.cookie.set( topLinksCookieName, 'collapsed' );
+                               toplinksTitle.setIndicator( 'down' );
+                               widget.switchTopLinks( 'collapsed' );
+                       } );
+
+               this.$topLinks.find( '.mw-recentchanges-toplinks-title' )
+                       .replaceWith( toplinksTitle.$element.removeAttr( 'tabIndex' ) );
+
+               // Create two positions for the toplinks to toggle between
+               // in the table (first cell) or up above it
+               this.$top = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-top' );
+               this.$tableTopLinks = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-cell' )
+                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-table' );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               this.$tableTopLinks,
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-table-placeholder' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' ),
+                                                               !mw.user.isAnon() ?
+                                                                       $( '<div>' )
+                                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                                               .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-savedLinks' )
+                                                                               .append( savedLinksListWidget.$element ) :
+                                                                       null
+                                                       )
+                                       )
+                       );
+
+               // Hack: For jumpiness reasons, this should be a sibling of -head
+               $( '.rcfilters-head' ).before( this.$top );
+
+               // Initialize top links position
+               widget.switchTopLinks( topLinksCookieValue );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( RcTopSectionWidget, OO.ui.Widget );
+
+       /**
+        * Switch the top links widget from inside the table (when collapsed)
+        * to the 'top' (when open)
+        *
+        * @param {string} [state] The state of the top links widget: 'expanded' or 'collapsed'
+        */
+       RcTopSectionWidget.prototype.switchTopLinks = function ( state ) {
+               state = state || 'expanded';
+
+               if ( state === 'expanded' ) {
+                       this.$top.append( this.$topLinks );
+               } else {
+                       this.$tableTopLinks.append( this.$topLinks );
+               }
+               this.$topLinks.toggleClass( 'mw-recentchanges-toplinks-collapsed', state === 'collapsed' );
+       };
+
+       module.exports = RcTopSectionWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/RclTargetPageWidget.js b/resources/src/mediawiki.rcfilters/ui/RclTargetPageWidget.js
new file mode 100644 (file)
index 0000000..6eb0d5b
--- /dev/null
@@ -0,0 +1,82 @@
+( function () {
+       /**
+        * Widget to select and display target page on Special:RecentChangesLinked (AKA Related Changes)
+        *
+        * @class mw.rcfilters.ui.RclTargetPageWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FilterItem} targetPageModel
+        * @param {Object} [config] Configuration object
+        */
+       var RclTargetPageWidget = function MwRcfiltersUiRclTargetPageWidget(
+               controller, targetPageModel, config
+       ) {
+               config = config || {};
+
+               // Parent
+               RclTargetPageWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = targetPageModel;
+
+               this.titleSearch = new mw.widgets.TitleInputWidget( {
+                       validate: false,
+                       placeholder: mw.msg( 'rcfilters-target-page-placeholder' ),
+                       showImages: true,
+                       showDescriptions: true,
+                       addQueryInput: false
+               } );
+
+               // Events
+               this.model.connect( this, { update: 'updateUiBasedOnModel' } );
+
+               this.titleSearch.$input.on( {
+                       blur: this.onLookupInputBlur.bind( this )
+               } );
+
+               this.titleSearch.lookupMenu.connect( this, {
+                       choose: 'onLookupMenuItemChoose'
+               } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-rclTargetPageWidget' )
+                       .append( this.titleSearch.$element );
+
+               this.updateUiBasedOnModel();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( RclTargetPageWidget, OO.ui.Widget );
+
+       /* Methods */
+
+       /**
+        * Respond to the user choosing a title
+        */
+       RclTargetPageWidget.prototype.onLookupMenuItemChoose = function () {
+               this.titleSearch.$input.trigger( 'blur' );
+       };
+
+       /**
+        * Respond to titleSearch $input blur
+        */
+       RclTargetPageWidget.prototype.onLookupInputBlur = function () {
+               this.controller.setTargetPage( this.titleSearch.getQueryValue() );
+       };
+
+       /**
+        * Respond to the model being updated
+        */
+       RclTargetPageWidget.prototype.updateUiBasedOnModel = function () {
+               var title = mw.Title.newFromText( this.model.getValue() ),
+                       text = title ? title.toText() : this.model.getValue();
+               this.titleSearch.setValue( text );
+               this.titleSearch.setTitle( text );
+       };
+
+       module.exports = RclTargetPageWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/RclToOrFromWidget.js b/resources/src/mediawiki.rcfilters/ui/RclToOrFromWidget.js
new file mode 100644 (file)
index 0000000..e2c58d0
--- /dev/null
@@ -0,0 +1,76 @@
+( function () {
+       /**
+        * Widget to select to view changes that link TO or FROM the target page
+        * on Special:RecentChangesLinked (AKA Related Changes)
+        *
+        * @class mw.rcfilters.ui.RclToOrFromWidget
+        * @extends OO.ui.DropdownWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel model this widget is bound to
+        * @param {Object} [config] Configuration object
+        */
+       var RclToOrFromWidget = function MwRcfiltersUiRclToOrFromWidget(
+               controller, showLinkedToModel, config
+       ) {
+               config = config || {};
+
+               this.showLinkedFrom = new OO.ui.MenuOptionWidget( {
+                       data: 'from', // default (showlinkedto=0)
+                       label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedfrom-option-label' ) )
+               } );
+               this.showLinkedTo = new OO.ui.MenuOptionWidget( {
+                       data: 'to', // showlinkedto=1
+                       label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedto-option-label' ) )
+               } );
+
+               // Parent
+               RclToOrFromWidget.parent.call( this, $.extend( {
+                       classes: [ 'mw-rcfilters-ui-rclToOrFromWidget' ],
+                       menu: { items: [ this.showLinkedFrom, this.showLinkedTo ] }
+               }, config ) );
+
+               this.controller = controller;
+               this.model = showLinkedToModel;
+
+               this.getMenu().connect( this, { choose: 'onUserChooseItem' } );
+               this.model.connect( this, { update: 'onModelUpdate' } );
+
+               // force an initial update of the component based on the state
+               this.onModelUpdate();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( RclToOrFromWidget, OO.ui.DropdownWidget );
+
+       /* Methods */
+
+       /**
+        * Respond to the user choosing an item in the menu
+        *
+        * @param {OO.ui.MenuOptionWidget} chosenItem
+        */
+       RclToOrFromWidget.prototype.onUserChooseItem = function ( chosenItem ) {
+               this.controller.setShowLinkedTo( chosenItem.getData() === 'to' );
+       };
+
+       /**
+        * Respond to model update
+        */
+       RclToOrFromWidget.prototype.onModelUpdate = function () {
+               this.getMenu().selectItem(
+                       this.model.isSelected() ?
+                               this.showLinkedTo :
+                               this.showLinkedFrom
+               );
+               this.setLabel( mw.msg(
+                       this.model.isSelected() ?
+                               'rcfilters-filter-showlinkedto-label' :
+                               'rcfilters-filter-showlinkedfrom-label'
+               ) );
+       };
+
+       module.exports = RclToOrFromWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/RclTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/RclTopSectionWidget.js
new file mode 100644 (file)
index 0000000..d968b9e
--- /dev/null
@@ -0,0 +1,73 @@
+( function () {
+       var RclToOrFromWidget = require( './RclToOrFromWidget.js' ),
+               RclTargetPageWidget = require( './RclTargetPageWidget.js' ),
+               RclTopSectionWidget;
+
+       /**
+        * Top section (between page title and filters) on Special:RecentChangesLinked (AKA RelatedChanges)
+        *
+        * @class mw.rcfilters.ui.RclTopSectionWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel Model for 'showlinkedto' parameter
+        * @param {mw.rcfilters.dm.FilterItem} targetPageModel Model for 'target' parameter
+        * @param {Object} [config] Configuration object
+        */
+       RclTopSectionWidget = function MwRcfiltersUiRclTopSectionWidget(
+               savedLinksListWidget, controller, showLinkedToModel, targetPageModel, config
+       ) {
+               var toOrFromWidget,
+                       targetPage;
+               config = config || {};
+
+               // Parent
+               RclTopSectionWidget.parent.call( this, config );
+
+               this.controller = controller;
+
+               toOrFromWidget = new RclToOrFromWidget( controller, showLinkedToModel );
+               targetPage = new RclTargetPageWidget( controller, targetPageModel );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-rclTopSectionWidget' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .append( toOrFromWidget.$element )
+                                                       ),
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .append( targetPage.$element ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-table-placeholder' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' ),
+                                                               !mw.user.isAnon() ?
+                                                                       $( '<div>' )
+                                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                                               .addClass( 'mw-rcfilters-ui-rclTopSectionWidget-savedLinks' )
+                                                                               .append( savedLinksListWidget.$element ) :
+                                                                       null
+                                                       )
+                                       )
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( RclTopSectionWidget, OO.ui.Widget );
+
+       module.exports = RclTopSectionWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/SaveFiltersPopupButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/SaveFiltersPopupButtonWidget.js
new file mode 100644 (file)
index 0000000..8c3d550
--- /dev/null
@@ -0,0 +1,191 @@
+( function () {
+       /**
+        * Save filters widget. This widget is displayed in the tag area
+        * and allows the user to save the current state of the system
+        * as a new saved filter query they can later load or set as
+        * default.
+        *
+        * @class mw.rcfilters.ui.SaveFiltersPopupButtonWidget
+        * @extends OO.ui.PopupButtonWidget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
+        * @param {Object} [config] Configuration object
+        */
+       var SaveFiltersPopupButtonWidget = function MwRcfiltersUiSaveFiltersPopupButtonWidget( controller, model, config ) {
+               var layout,
+                       checkBoxLayout,
+                       $popupContent = $( '<div>' );
+
+               config = config || {};
+
+               this.controller = controller;
+               this.model = model;
+
+               // Parent
+               SaveFiltersPopupButtonWidget.parent.call( this, $.extend( {
+                       framed: false,
+                       icon: 'bookmark',
+                       title: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
+                       popup: {
+                               classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup' ],
+                               padded: true,
+                               head: true,
+                               label: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
+                               $content: $popupContent
+                       }
+               }, config ) );
+               // // HACK: Add an icon to the popup head label
+               this.popup.$head.prepend( ( new OO.ui.IconWidget( { icon: 'bookmark' } ) ).$element );
+
+               this.input = new OO.ui.TextInputWidget( {
+                       placeholder: mw.msg( 'rcfilters-savedqueries-new-name-placeholder' )
+               } );
+               layout = new OO.ui.FieldLayout( this.input, {
+                       label: mw.msg( 'rcfilters-savedqueries-new-name-label' ),
+                       align: 'top'
+               } );
+
+               this.setAsDefaultCheckbox = new OO.ui.CheckboxInputWidget();
+               checkBoxLayout = new OO.ui.FieldLayout( this.setAsDefaultCheckbox, {
+                       label: mw.msg( 'rcfilters-savedqueries-setdefault' ),
+                       align: 'inline'
+               } );
+
+               this.applyButton = new OO.ui.ButtonWidget( {
+                       label: mw.msg( 'rcfilters-savedqueries-apply-label' ),
+                       classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-apply' ],
+                       flags: [ 'primary', 'progressive' ]
+               } );
+               this.cancelButton = new OO.ui.ButtonWidget( {
+                       label: mw.msg( 'rcfilters-savedqueries-cancel-label' ),
+                       classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-cancel' ]
+               } );
+
+               $popupContent
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-layout' )
+                                       .append( layout.$element ),
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-options' )
+                                       .append( checkBoxLayout.$element ),
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons' )
+                                       .append(
+                                               this.cancelButton.$element,
+                                               this.applyButton.$element
+                                       )
+                       );
+
+               // Events
+               this.popup.connect( this, {
+                       ready: 'onPopupReady'
+               } );
+               this.input.connect( this, {
+                       change: 'onInputChange',
+                       enter: 'onInputEnter'
+               } );
+               this.input.$input.on( {
+                       keyup: this.onInputKeyup.bind( this )
+               } );
+               this.setAsDefaultCheckbox.connect( this, { change: 'onSetAsDefaultChange' } );
+               this.cancelButton.connect( this, { click: 'onCancelButtonClick' } );
+               this.applyButton.connect( this, { click: 'onApplyButtonClick' } );
+
+               // Initialize
+               this.applyButton.setDisabled( !this.input.getValue() );
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget' );
+       };
+
+       /* Initialization */
+       OO.inheritClass( SaveFiltersPopupButtonWidget, OO.ui.PopupButtonWidget );
+
+       /**
+        * Respond to input enter event
+        */
+       SaveFiltersPopupButtonWidget.prototype.onInputEnter = function () {
+               this.apply();
+       };
+
+       /**
+        * Respond to input change event
+        *
+        * @param {string} value Input value
+        */
+       SaveFiltersPopupButtonWidget.prototype.onInputChange = function ( value ) {
+               value = value.trim();
+
+               this.applyButton.setDisabled( !value );
+       };
+
+       /**
+        * Respond to input keyup event, this is the way to intercept 'escape' key
+        *
+        * @param {jQuery.Event} e Event data
+        * @return {boolean} false
+        */
+       SaveFiltersPopupButtonWidget.prototype.onInputKeyup = function ( e ) {
+               if ( e.which === OO.ui.Keys.ESCAPE ) {
+                       this.popup.toggle( false );
+                       return false;
+               }
+       };
+
+       /**
+        * Respond to popup ready event
+        */
+       SaveFiltersPopupButtonWidget.prototype.onPopupReady = function () {
+               this.input.focus();
+       };
+
+       /**
+        * Respond to "set as default" checkbox change
+        * @param {boolean} checked State of the checkbox
+        */
+       SaveFiltersPopupButtonWidget.prototype.onSetAsDefaultChange = function ( checked ) {
+               var messageKey = checked ?
+                       'rcfilters-savedqueries-apply-and-setdefault-label' :
+                       'rcfilters-savedqueries-apply-label';
+
+               this.applyButton
+                       .setIcon( checked ? 'pushPin' : null )
+                       .setLabel( mw.msg( messageKey ) );
+       };
+
+       /**
+        * Respond to cancel button click event
+        */
+       SaveFiltersPopupButtonWidget.prototype.onCancelButtonClick = function () {
+               this.popup.toggle( false );
+       };
+
+       /**
+        * Respond to apply button click event
+        */
+       SaveFiltersPopupButtonWidget.prototype.onApplyButtonClick = function () {
+               this.apply();
+       };
+
+       /**
+        * Apply and add the new quick link
+        */
+       SaveFiltersPopupButtonWidget.prototype.apply = function () {
+               var label = this.input.getValue().trim();
+
+               // This condition is more for sanity-check, since the
+               // apply button should be disabled if the label is empty
+               if ( label ) {
+                       this.controller.saveCurrentQuery( label, this.setAsDefaultCheckbox.isSelected() );
+                       this.input.setValue( '' );
+                       this.setAsDefaultCheckbox.setSelected( false );
+                       this.popup.toggle( false );
+
+                       this.emit( 'saveCurrent' );
+               }
+       };
+
+       module.exports = SaveFiltersPopupButtonWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/SavedLinksListItemWidget.js b/resources/src/mediawiki.rcfilters/ui/SavedLinksListItemWidget.js
new file mode 100644 (file)
index 0000000..ceb5ef8
--- /dev/null
@@ -0,0 +1,333 @@
+( function () {
+       /**
+        * Quick links menu option widget
+        *
+        * @class mw.rcfilters.ui.SavedLinksListItemWidget
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.LabelElement
+        * @mixins OO.ui.mixin.IconElement
+        * @mixins OO.ui.mixin.TitledElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.SavedQueryItemModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       var SavedLinksListItemWidget = function MwRcfiltersUiSavedLinksListWidget( model, config ) {
+               config = config || {};
+
+               this.model = model;
+
+               // Parent
+               SavedLinksListItemWidget.parent.call( this, $.extend( {
+                       data: this.model.getID()
+               }, config ) );
+
+               // Mixin constructors
+               OO.ui.mixin.LabelElement.call( this, $.extend( {
+                       label: this.model.getLabel()
+               }, config ) );
+               OO.ui.mixin.IconElement.call( this, $.extend( {
+                       icon: ''
+               }, config ) );
+               OO.ui.mixin.TitledElement.call( this, $.extend( {
+                       title: this.model.getLabel()
+               }, config ) );
+
+               this.edit = false;
+               this.$overlay = config.$overlay || this.$element;
+
+               this.popupButton = new OO.ui.ButtonWidget( {
+                       classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-button' ],
+                       icon: 'ellipsis',
+                       framed: false
+               } );
+               this.menu = new OO.ui.MenuSelectWidget( {
+                       classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-menu' ],
+                       widget: this.popupButton,
+                       width: 200,
+                       horizontalPosition: 'end',
+                       $floatableContainer: this.popupButton.$element,
+                       items: [
+                               new OO.ui.MenuOptionWidget( {
+                                       data: 'edit',
+                                       icon: 'edit',
+                                       label: mw.msg( 'rcfilters-savedqueries-rename' )
+                               } ),
+                               new OO.ui.MenuOptionWidget( {
+                                       data: 'delete',
+                                       icon: 'trash',
+                                       label: mw.msg( 'rcfilters-savedqueries-remove' )
+                               } ),
+                               new OO.ui.MenuOptionWidget( {
+                                       data: 'default',
+                                       icon: 'pushPin',
+                                       label: mw.msg( 'rcfilters-savedqueries-setdefault' )
+                               } )
+                       ]
+               } );
+
+               this.editInput = new OO.ui.TextInputWidget( {
+                       classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-input' ]
+               } );
+               this.saveButton = new OO.ui.ButtonWidget( {
+                       icon: 'check',
+                       flags: [ 'primary', 'progressive' ]
+               } );
+               this.toggleEdit( false );
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+               this.popupButton.connect( this, { click: 'onPopupButtonClick' } );
+               this.menu.connect( this, {
+                       choose: 'onMenuChoose'
+               } );
+               this.saveButton.connect( this, { click: 'save' } );
+               this.editInput.connect( this, {
+                       change: 'onInputChange',
+                       enter: 'save'
+               } );
+               this.editInput.$input.on( {
+                       blur: this.onInputBlur.bind( this ),
+                       keyup: this.onInputKeyup.bind( this )
+               } );
+               this.$element.on( { click: this.onClick.bind( this ) } );
+               this.$label.on( { click: this.onClick.bind( this ) } );
+               this.$icon.on( { click: this.onDefaultIconClick.bind( this ) } );
+               // Prevent propagation on mousedown for the save button
+               // so the menu doesn't close
+               this.saveButton.$element.on( { mousedown: function () {
+                       return false;
+               } } );
+
+               // Initialize
+               this.toggleDefault( !!this.model.isDefault() );
+               this.$overlay.append( this.menu.$element );
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget' )
+                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-query-' + this.model.getID() )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-content' )
+                                                                       .append(
+                                                                               this.$label
+                                                                                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-label' ),
+                                                                               this.editInput.$element,
+                                                                               this.saveButton.$element
+                                                                       ),
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-icon' )
+                                                                       .append( this.$icon ),
+                                                               this.popupButton.$element
+                                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       )
+                                       )
+                       );
+       };
+
+       /* Initialization */
+       OO.inheritClass( SavedLinksListItemWidget, OO.ui.Widget );
+       OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.LabelElement );
+       OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.IconElement );
+       OO.mixinClass( SavedLinksListItemWidget, OO.ui.mixin.TitledElement );
+
+       /* Events */
+
+       /**
+        * @event delete
+        *
+        * The delete option was selected for this item
+        */
+
+       /**
+        * @event default
+        * @param {boolean} default Item is default
+        *
+        * The 'make default' option was selected for this item
+        */
+
+       /**
+        * @event edit
+        * @param {string} newLabel New label for the query
+        *
+        * The label has been edited
+        */
+
+       /* Methods */
+
+       /**
+        * Respond to model update event
+        */
+       SavedLinksListItemWidget.prototype.onModelUpdate = function () {
+               this.setLabel( this.model.getLabel() );
+               this.toggleDefault( this.model.isDefault() );
+       };
+
+       /**
+        * Respond to click on the element or label
+        *
+        * @fires click
+        */
+       SavedLinksListItemWidget.prototype.onClick = function () {
+               if ( !this.editing ) {
+                       this.emit( 'click' );
+               }
+       };
+
+       /**
+        * Respond to click on the 'default' icon. Open the submenu where the
+        * default state can be changed.
+        *
+        * @return {boolean} false
+        */
+       SavedLinksListItemWidget.prototype.onDefaultIconClick = function () {
+               this.menu.toggle();
+               return false;
+       };
+
+       /**
+        * Respond to popup button click event
+        */
+       SavedLinksListItemWidget.prototype.onPopupButtonClick = function () {
+               this.menu.toggle();
+       };
+
+       /**
+        * Respond to menu choose event
+        *
+        * @param {OO.ui.MenuOptionWidget} item Chosen item
+        * @fires delete
+        * @fires default
+        */
+       SavedLinksListItemWidget.prototype.onMenuChoose = function ( item ) {
+               var action = item.getData();
+
+               if ( action === 'edit' ) {
+                       this.toggleEdit( true );
+               } else if ( action === 'delete' ) {
+                       this.emit( 'delete' );
+               } else if ( action === 'default' ) {
+                       this.emit( 'default', !this.default );
+               }
+               // Reset selected
+               this.menu.selectItem( null );
+               // Close the menu
+               this.menu.toggle( false );
+       };
+
+       /**
+        * Respond to input keyup event, this is the way to intercept 'escape' key
+        *
+        * @param {jQuery.Event} e Event data
+        * @return {boolean} false
+        */
+       SavedLinksListItemWidget.prototype.onInputKeyup = function ( e ) {
+               if ( e.which === OO.ui.Keys.ESCAPE ) {
+                       // Return the input to the original label
+                       this.editInput.setValue( this.getLabel() );
+                       this.toggleEdit( false );
+                       return false;
+               }
+       };
+
+       /**
+        * Respond to blur event on the input
+        */
+       SavedLinksListItemWidget.prototype.onInputBlur = function () {
+               this.save();
+
+               // Whether the save succeeded or not, the input-blur event
+               // means we need to cancel editing mode
+               this.toggleEdit( false );
+       };
+
+       /**
+        * Respond to input change event
+        *
+        * @param {string} value Input value
+        */
+       SavedLinksListItemWidget.prototype.onInputChange = function ( value ) {
+               value = value.trim();
+
+               this.saveButton.setDisabled( !value );
+       };
+
+       /**
+        * Save the name of the query
+        *
+        * @param {string} [value] The value to save
+        * @fires edit
+        */
+       SavedLinksListItemWidget.prototype.save = function () {
+               var value = this.editInput.getValue().trim();
+
+               if ( value ) {
+                       this.emit( 'edit', value );
+                       this.toggleEdit( false );
+               }
+       };
+
+       /**
+        * Toggle edit mode on this widget
+        *
+        * @param {boolean} isEdit Widget is in edit mode
+        */
+       SavedLinksListItemWidget.prototype.toggleEdit = function ( isEdit ) {
+               isEdit = isEdit === undefined ? !this.editing : isEdit;
+
+               if ( this.editing !== isEdit ) {
+                       this.$element.toggleClass( 'mw-rcfilters-ui-savedLinksListItemWidget-edit', isEdit );
+                       this.editInput.setValue( this.getLabel() );
+
+                       this.editInput.toggle( isEdit );
+                       this.$label.toggleClass( 'oo-ui-element-hidden', isEdit );
+                       this.$icon.toggleClass( 'oo-ui-element-hidden', isEdit );
+                       this.popupButton.toggle( !isEdit );
+                       this.saveButton.toggle( isEdit );
+
+                       if ( isEdit ) {
+                               this.editInput.$input.trigger( 'focus' );
+                       }
+                       this.editing = isEdit;
+               }
+       };
+
+       /**
+        * Toggle default this widget
+        *
+        * @param {boolean} isDefault This item is default
+        */
+       SavedLinksListItemWidget.prototype.toggleDefault = function ( isDefault ) {
+               isDefault = isDefault === undefined ? !this.default : isDefault;
+
+               if ( this.default !== isDefault ) {
+                       this.default = isDefault;
+                       this.setIcon( this.default ? 'pushPin' : '' );
+                       this.menu.findItemFromData( 'default' ).setLabel(
+                               this.default ?
+                                       mw.msg( 'rcfilters-savedqueries-unsetdefault' ) :
+                                       mw.msg( 'rcfilters-savedqueries-setdefault' )
+                       );
+               }
+       };
+
+       /**
+        * Get item ID
+        *
+        * @return {string} Query identifier
+        */
+       SavedLinksListItemWidget.prototype.getID = function () {
+               return this.model.getID();
+       };
+
+       module.exports = SavedLinksListItemWidget;
+
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/SavedLinksListWidget.js b/resources/src/mediawiki.rcfilters/ui/SavedLinksListWidget.js
new file mode 100644 (file)
index 0000000..5422daf
--- /dev/null
@@ -0,0 +1,159 @@
+( function () {
+       var GroupWidget = require( './GroupWidget.js' ),
+               SavedLinksListItemWidget = require( './SavedLinksListItemWidget.js' ),
+               SavedLinksListWidget;
+
+       /**
+        * Quick links widget
+        *
+        * @class mw.rcfilters.ui.SavedLinksListWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
+        * @param {Object} [config] Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       SavedLinksListWidget = function MwRcfiltersUiSavedLinksListWidget( controller, model, config ) {
+               var $labelNoEntries = $( '<div>' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-title' )
+                                       .text( mw.msg( 'rcfilters-quickfilters-placeholder-title' ) ),
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-description' )
+                                       .text( mw.msg( 'rcfilters-quickfilters-placeholder-description' ) )
+                       );
+
+               config = config || {};
+
+               // Parent
+               SavedLinksListWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+               this.$overlay = config.$overlay || this.$element;
+
+               this.placeholderItem = new OO.ui.DecoratedOptionWidget( {
+                       classes: [ 'mw-rcfilters-ui-savedLinksListWidget-placeholder' ],
+                       label: $labelNoEntries,
+                       icon: 'bookmark'
+               } );
+
+               this.menu = new GroupWidget( {
+                       events: {
+                               click: 'menuItemClick',
+                               delete: 'menuItemDelete',
+                               default: 'menuItemDefault',
+                               edit: 'menuItemEdit'
+                       },
+                       classes: [ 'mw-rcfilters-ui-savedLinksListWidget-menu' ],
+                       items: [ this.placeholderItem ]
+               } );
+               this.button = new OO.ui.PopupButtonWidget( {
+                       classes: [ 'mw-rcfilters-ui-savedLinksListWidget-button' ],
+                       label: mw.msg( 'rcfilters-quickfilters' ),
+                       icon: 'bookmark',
+                       indicator: 'down',
+                       $overlay: this.$overlay,
+                       popup: {
+                               width: 300,
+                               anchor: false,
+                               align: 'backwards',
+                               $autoCloseIgnore: this.$overlay,
+                               $content: this.menu.$element
+                       }
+               } );
+
+               // Events
+               this.model.connect( this, {
+                       add: 'onModelAddItem',
+                       remove: 'onModelRemoveItem'
+               } );
+               this.menu.connect( this, {
+                       menuItemClick: 'onMenuItemClick',
+                       menuItemDelete: 'onMenuItemRemove',
+                       menuItemDefault: 'onMenuItemDefault',
+                       menuItemEdit: 'onMenuItemEdit'
+               } );
+
+               this.placeholderItem.toggle( this.model.isEmpty() );
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-savedLinksListWidget' )
+                       .append( this.button.$element );
+       };
+
+       /* Initialization */
+       OO.inheritClass( SavedLinksListWidget, OO.ui.Widget );
+
+       /* Methods */
+
+       /**
+        * Respond to menu item click event
+        *
+        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+        */
+       SavedLinksListWidget.prototype.onMenuItemClick = function ( item ) {
+               this.controller.applySavedQuery( item.getID() );
+               this.button.popup.toggle( false );
+       };
+
+       /**
+        * Respond to menu item remove event
+        *
+        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+        */
+       SavedLinksListWidget.prototype.onMenuItemRemove = function ( item ) {
+               this.controller.removeSavedQuery( item.getID() );
+       };
+
+       /**
+        * Respond to menu item default event
+        *
+        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+        * @param {boolean} isDefault Item is default
+        */
+       SavedLinksListWidget.prototype.onMenuItemDefault = function ( item, isDefault ) {
+               this.controller.setDefaultSavedQuery( isDefault ? item.getID() : null );
+       };
+
+       /**
+        * Respond to menu item edit event
+        *
+        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+        * @param {string} newLabel New label
+        */
+       SavedLinksListWidget.prototype.onMenuItemEdit = function ( item, newLabel ) {
+               this.controller.renameSavedQuery( item.getID(), newLabel );
+       };
+
+       /**
+        * Respond to menu add item event
+        *
+        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+        */
+       SavedLinksListWidget.prototype.onModelAddItem = function ( item ) {
+               if ( this.menu.findItemFromData( item.getID() ) ) {
+                       return;
+               }
+
+               this.menu.addItems( [
+                       new SavedLinksListItemWidget( item, { $overlay: this.$overlay } )
+               ] );
+               this.placeholderItem.toggle( this.model.isEmpty() );
+       };
+
+       /**
+        * Respond to menu remove item event
+        *
+        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
+        */
+       SavedLinksListWidget.prototype.onModelRemoveItem = function ( item ) {
+               this.menu.removeItems( [ this.menu.findItemFromData( item.getID() ) ] );
+               this.placeholderItem.toggle( this.model.isEmpty() );
+       };
+
+       module.exports = SavedLinksListWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/TagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/TagItemWidget.js
new file mode 100644 (file)
index 0000000..d66c5b5
--- /dev/null
@@ -0,0 +1,225 @@
+( function () {
+       /**
+        * Extend OOUI's TagItemWidget to also display a popup on hover.
+        *
+        * @class mw.rcfilters.ui.TagItemWidget
+        * @extends OO.ui.TagItemWidget
+        * @mixins OO.ui.mixin.PopupElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
+        * @param {mw.rcfilters.dm.FilterItem} invertModel
+        * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
+        * @param {Object} config Configuration object
+        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+        */
+       var TagItemWidget = function MwRcfiltersUiTagItemWidget(
+               controller, filtersViewModel, invertModel, itemModel, config
+       ) {
+               // Configuration initialization
+               config = config || {};
+
+               this.controller = controller;
+               this.invertModel = invertModel;
+               this.filtersViewModel = filtersViewModel;
+               this.itemModel = itemModel;
+               this.selected = false;
+
+               TagItemWidget.parent.call( this, $.extend( {
+                       data: this.itemModel.getName()
+               }, config ) );
+
+               this.$overlay = config.$overlay || this.$element;
+               this.popupLabel = new OO.ui.LabelWidget();
+
+               // Mixin constructors
+               OO.ui.mixin.PopupElement.call( this, $.extend( {
+                       popup: {
+                               padded: false,
+                               align: 'center',
+                               position: 'above',
+                               $content: $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-tagItemWidget-popup-content' )
+                                       .append( this.popupLabel.$element ),
+                               $floatableContainer: this.$element,
+                               classes: [ 'mw-rcfilters-ui-tagItemWidget-popup' ]
+                       }
+               }, config ) );
+
+               this.popupTimeoutShow = null;
+               this.popupTimeoutHide = null;
+
+               this.$highlight = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-tagItemWidget-highlight' );
+
+               // Add title attribute with the item label to 'x' button
+               this.closeButton.setTitle( mw.msg( 'rcfilters-tag-remove', this.itemModel.getLabel() ) );
+
+               // Events
+               this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
+               this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
+               this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
+
+               // Initialization
+               this.$overlay.append( this.popup.$element );
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-tagItemWidget' )
+                       .prepend( this.$highlight )
+                       .attr( 'aria-haspopup', 'true' )
+                       .on( 'mouseenter', this.onMouseEnter.bind( this ) )
+                       .on( 'mouseleave', this.onMouseLeave.bind( this ) );
+
+               this.updateUiBasedOnState();
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( TagItemWidget, OO.ui.TagItemWidget );
+       OO.mixinClass( TagItemWidget, OO.ui.mixin.PopupElement );
+
+       /* Methods */
+
+       /**
+        * Respond to model update event
+        */
+       TagItemWidget.prototype.updateUiBasedOnState = function () {
+               // Update label if needed
+               var labelMsg = this.itemModel.getLabelMessageKey( this.invertModel.isSelected() );
+               if ( labelMsg ) {
+                       this.setLabel( $( '<div>' ).append(
+                               $( '<bdi>' ).html(
+                                       mw.message( labelMsg, mw.html.escape( this.itemModel.getLabel() ) ).parse()
+                               )
+                       ).contents() );
+               } else {
+                       this.setLabel(
+                               $( '<bdi>' ).append(
+                                       this.itemModel.getLabel()
+                               )
+                       );
+               }
+
+               this.setCurrentMuteState();
+               this.setHighlightColor();
+       };
+
+       /**
+        * Set the current highlight color for this item
+        */
+       TagItemWidget.prototype.setHighlightColor = function () {
+               var selectedColor = this.filtersViewModel.isHighlightEnabled() && this.itemModel.isHighlighted ?
+                       this.itemModel.getHighlightColor() :
+                       null;
+
+               this.$highlight
+                       .attr( 'data-color', selectedColor )
+                       .toggleClass(
+                               'mw-rcfilters-ui-tagItemWidget-highlight-highlighted',
+                               !!selectedColor
+                       );
+       };
+
+       /**
+        * Set the current mute state for this item
+        */
+       TagItemWidget.prototype.setCurrentMuteState = function () {};
+
+       /**
+        * Respond to mouse enter event
+        */
+       TagItemWidget.prototype.onMouseEnter = function () {
+               var labelText = this.itemModel.getStateMessage();
+
+               if ( labelText ) {
+                       this.popupLabel.setLabel( labelText );
+
+                       // Set timeout for the popup to show
+                       this.popupTimeoutShow = setTimeout( function () {
+                               this.popup.toggle( true );
+                       }.bind( this ), 500 );
+
+                       // Cancel the hide timeout
+                       clearTimeout( this.popupTimeoutHide );
+                       this.popupTimeoutHide = null;
+               }
+       };
+
+       /**
+        * Respond to mouse leave event
+        */
+       TagItemWidget.prototype.onMouseLeave = function () {
+               this.popupTimeoutHide = setTimeout( function () {
+                       this.popup.toggle( false );
+               }.bind( this ), 250 );
+
+               // Clear the show timeout
+               clearTimeout( this.popupTimeoutShow );
+               this.popupTimeoutShow = null;
+       };
+
+       /**
+        * Set selected state on this widget
+        *
+        * @param {boolean} [isSelected] Widget is selected
+        */
+       TagItemWidget.prototype.toggleSelected = function ( isSelected ) {
+               isSelected = isSelected !== undefined ? isSelected : !this.selected;
+
+               if ( this.selected !== isSelected ) {
+                       this.selected = isSelected;
+
+                       this.$element.toggleClass( 'mw-rcfilters-ui-tagItemWidget-selected', this.selected );
+               }
+       };
+
+       /**
+        * Get the selected state of this widget
+        *
+        * @return {boolean} Tag is selected
+        */
+       TagItemWidget.prototype.isSelected = function () {
+               return this.selected;
+       };
+
+       /**
+        * Get item name
+        *
+        * @return {string} Filter name
+        */
+       TagItemWidget.prototype.getName = function () {
+               return this.itemModel.getName();
+       };
+
+       /**
+        * Get item model
+        *
+        * @return {string} Filter model
+        */
+       TagItemWidget.prototype.getModel = function () {
+               return this.itemModel;
+       };
+
+       /**
+        * Get item view
+        *
+        * @return {string} Filter view
+        */
+       TagItemWidget.prototype.getView = function () {
+               return this.itemModel.getGroupModel().getView();
+       };
+
+       /**
+        * Remove and destroy external elements of this widget
+        */
+       TagItemWidget.prototype.destroy = function () {
+               // Destroy the popup
+               this.popup.$element.detach();
+
+               // Disconnect events
+               this.itemModel.disconnect( this );
+               this.closeButton.disconnect( this );
+       };
+
+       module.exports = TagItemWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/ValuePickerWidget.js b/resources/src/mediawiki.rcfilters/ui/ValuePickerWidget.js
new file mode 100644 (file)
index 0000000..ebd81c8
--- /dev/null
@@ -0,0 +1,114 @@
+( function () {
+       /**
+        * Widget defining the behavior used to choose from a set of values
+        * in a single_value group
+        *
+        * @class mw.rcfilters.ui.ValuePickerWidget
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.LabelElement
+        *
+        * @constructor
+        * @param {mw.rcfilters.dm.FilterGroup} model Group model
+        * @param {Object} [config] Configuration object
+        * @cfg {Function} [itemFilter] A filter function for the items from the
+        *  model. If not given, all items will be included. The function must
+        *  handle item models and return a boolean whether the item is included
+        *  or not. Example: function ( itemModel ) { return itemModel.isSelected(); }
+        */
+       var ValuePickerWidget = function MwRcfiltersUiValuePickerWidget( model, config ) {
+               config = config || {};
+
+               // Parent
+               ValuePickerWidget.parent.call( this, config );
+               // Mixin constructors
+               OO.ui.mixin.LabelElement.call( this, config );
+
+               this.model = model;
+               this.itemFilter = config.itemFilter || function () {
+                       return true;
+               };
+
+               // Build the selection from the item models
+               this.selectWidget = new OO.ui.ButtonSelectWidget();
+               this.initializeSelectWidget();
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+               this.selectWidget.connect( this, { choose: 'onSelectWidgetChoose' } );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-valuePickerWidget' )
+                       .append(
+                               this.$label
+                                       .addClass( 'mw-rcfilters-ui-valuePickerWidget-title' ),
+                               this.selectWidget.$element
+                       );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( ValuePickerWidget, OO.ui.Widget );
+       OO.mixinClass( ValuePickerWidget, OO.ui.mixin.LabelElement );
+
+       /* Events */
+
+       /**
+        * @event choose
+        * @param {string} name Item name
+        *
+        * An item has been chosen
+        */
+
+       /* Methods */
+
+       /**
+        * Respond to model update event
+        */
+       ValuePickerWidget.prototype.onModelUpdate = function () {
+               this.selectCurrentModelItem();
+       };
+
+       /**
+        * Respond to select widget choose event
+        *
+        * @param {OO.ui.ButtonOptionWidget} chosenItem Chosen item
+        * @fires choose
+        */
+       ValuePickerWidget.prototype.onSelectWidgetChoose = function ( chosenItem ) {
+               this.emit( 'choose', chosenItem.getData() );
+       };
+
+       /**
+        * Initialize the select widget
+        */
+       ValuePickerWidget.prototype.initializeSelectWidget = function () {
+               var items = this.model.getItems()
+                       .filter( this.itemFilter )
+                       .map( function ( filterItem ) {
+                               return new OO.ui.ButtonOptionWidget( {
+                                       data: filterItem.getName(),
+                                       label: filterItem.getLabel()
+                               } );
+                       } );
+
+               this.selectWidget.clearItems();
+               this.selectWidget.addItems( items );
+
+               this.selectCurrentModelItem();
+       };
+
+       /**
+        * Select the current item that corresponds with the model item
+        * that is currently selected
+        */
+       ValuePickerWidget.prototype.selectCurrentModelItem = function () {
+               var selectedItem = this.model.findSelectedItems()[ 0 ];
+
+               if ( selectedItem ) {
+                       this.selectWidget.selectItemByData( selectedItem.getName() );
+               }
+       };
+
+       module.exports = ValuePickerWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/ViewSwitchWidget.js b/resources/src/mediawiki.rcfilters/ui/ViewSwitchWidget.js
new file mode 100644 (file)
index 0000000..c00d414
--- /dev/null
@@ -0,0 +1,84 @@
+( function () {
+       var GroupWidget = require( './GroupWidget.js' ),
+               ViewSwitchWidget;
+
+       /**
+        * A widget for the footer for the default view, allowing to switch views
+        *
+        * @class mw.rcfilters.ui.ViewSwitchWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller Controller
+        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+        * @param {Object} [config] Configuration object
+        */
+       ViewSwitchWidget = function MwRcfiltersUiViewSwitchWidget( controller, model, config ) {
+               config = config || {};
+
+               // Parent
+               ViewSwitchWidget.parent.call( this, config );
+
+               this.controller = controller;
+               this.model = model;
+
+               this.buttons = new GroupWidget( {
+                       events: {
+                               click: 'buttonClick'
+                       },
+                       items: [
+                               new OO.ui.ButtonWidget( {
+                                       data: 'namespaces',
+                                       icon: 'article',
+                                       label: mw.msg( 'namespaces' )
+                               } ),
+                               new OO.ui.ButtonWidget( {
+                                       data: 'tags',
+                                       icon: 'tag',
+                                       label: mw.msg( 'rcfilters-view-tags' )
+                               } )
+                       ]
+               } );
+
+               // Events
+               this.model.connect( this, { update: 'onModelUpdate' } );
+               this.buttons.connect( this, { buttonClick: 'onButtonClick' } );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-viewSwitchWidget' )
+                       .append(
+                               new OO.ui.LabelWidget( {
+                                       label: mw.msg( 'rcfilters-advancedfilters' )
+                               } ).$element,
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-viewSwitchWidget-buttons' )
+                                       .append( this.buttons.$element )
+                       );
+       };
+
+       /* Initialize */
+
+       OO.inheritClass( ViewSwitchWidget, OO.ui.Widget );
+
+       /**
+        * Respond to model update event
+        */
+       ViewSwitchWidget.prototype.onModelUpdate = function () {
+               var currentView = this.model.getCurrentView();
+
+               this.buttons.getItems().forEach( function ( buttonWidget ) {
+                       buttonWidget.setActive( buttonWidget.getData() === currentView );
+               } );
+       };
+
+       /**
+        * Respond to button switch click
+        *
+        * @param {OO.ui.ButtonWidget} buttonWidget Clicked button
+        */
+       ViewSwitchWidget.prototype.onButtonClick = function ( buttonWidget ) {
+               this.controller.switchView( buttonWidget.getData() );
+       };
+
+       module.exports = ViewSwitchWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/WatchlistTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/WatchlistTopSectionWidget.js
new file mode 100644 (file)
index 0000000..16c0533
--- /dev/null
@@ -0,0 +1,88 @@
+( function () {
+       var MarkSeenButtonWidget = require( './MarkSeenButtonWidget.js' ),
+               WatchlistTopSectionWidget;
+       /**
+        * Top section (between page title and filters) on Special:Watchlist
+        *
+        * @class mw.rcfilters.ui.WatchlistTopSectionWidget
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+        * @param {jQuery} $watchlistDetails Content of the 'details' section that includes watched pages count
+        * @param {Object} [config] Configuration object
+        */
+       WatchlistTopSectionWidget = function MwRcfiltersUiWatchlistTopSectionWidget(
+               controller, changesListModel, savedLinksListWidget, $watchlistDetails, config
+       ) {
+               var editWatchlistButton,
+                       markSeenButton,
+                       $topTable,
+                       $bottomTable,
+                       $separator;
+               config = config || {};
+
+               // Parent
+               WatchlistTopSectionWidget.parent.call( this, config );
+
+               editWatchlistButton = new OO.ui.ButtonWidget( {
+                       label: mw.msg( 'rcfilters-watchlist-edit-watchlist-button' ),
+                       icon: 'edit',
+                       href: require( '../config.json' ).StructuredChangeFiltersEditWatchlistUrl
+               } );
+               markSeenButton = new MarkSeenButtonWidget( controller, changesListModel );
+
+               $topTable = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-table' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-row' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails' )
+                                                       .append( $watchlistDetails )
+                                       )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton' )
+                                                       .append( editWatchlistButton.$element )
+                                       )
+                       );
+
+               $bottomTable = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-table' )
+                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinksTable' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-row' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .append( markSeenButton.$element )
+                                       )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' )
+                                                       .append( savedLinksListWidget.$element )
+                                       )
+                       );
+
+               $separator = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-separator' );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget' )
+                       .append( $topTable, $separator, $bottomTable );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( WatchlistTopSectionWidget, OO.ui.Widget );
+
+       module.exports = WatchlistTopSectionWidget;
+}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.js
deleted file mode 100644 (file)
index e907a15..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-( function () {
-       /**
-        * Widget defining the button controlling the popup for the number of results
-        *
-        * @class
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {Object} [config] Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.ChangesLimitAndDateButtonWidget = function MwRcfiltersUiChangesLimitWidget( controller, model, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-
-               this.$overlay = config.$overlay || this.$element;
-
-               this.button = null;
-               this.limitGroupModel = null;
-               this.groupByPageItemModel = null;
-               this.daysGroupModel = null;
-
-               this.model.connect( this, {
-                       initialize: 'onModelInitialize'
-               } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-changesLimitAndDateButtonWidget' );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.ChangesLimitAndDateButtonWidget, OO.ui.Widget );
-
-       /**
-        * Respond to model initialize event
-        */
-       mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.prototype.onModelInitialize = function () {
-               var changesLimitPopupWidget, selectedItem, currentValue, datePopupWidget,
-                       displayGroupModel = this.model.getGroup( 'display' );
-
-               this.limitGroupModel = this.model.getGroup( 'limit' );
-               this.groupByPageItemModel = displayGroupModel.getItemByParamName( 'enhanced' );
-               this.daysGroupModel = this.model.getGroup( 'days' );
-
-               // HACK: We need the model to be ready before we populate the button
-               // and the widget, because we require the filter items for the
-               // limit and their events. This addition is only done after the
-               // model is initialized.
-               // Note: This will be fixed soon!
-               if ( this.limitGroupModel && this.daysGroupModel ) {
-                       changesLimitPopupWidget = new mw.rcfilters.ui.ChangesLimitPopupWidget(
-                               this.limitGroupModel,
-                               this.groupByPageItemModel
-                       );
-
-                       datePopupWidget = new mw.rcfilters.ui.DatePopupWidget(
-                               this.daysGroupModel,
-                               {
-                                       label: mw.msg( 'rcfilters-date-popup-title' )
-                               }
-                       );
-
-                       selectedItem = this.limitGroupModel.findSelectedItems()[ 0 ];
-                       currentValue = ( selectedItem && selectedItem.getLabel() ) ||
-                               mw.language.convertNumber( this.limitGroupModel.getDefaultParamValue() );
-
-                       this.button = new OO.ui.PopupButtonWidget( {
-                               icon: 'settings',
-                               indicator: 'down',
-                               label: mw.msg( 'rcfilters-limit-and-date-label', currentValue ),
-                               $overlay: this.$overlay,
-                               popup: {
-                                       width: 300,
-                                       padded: false,
-                                       anchor: false,
-                                       align: 'backwards',
-                                       $autoCloseIgnore: this.$overlay,
-                                       $content: $( '<div>' ).append(
-                                               // TODO: Merge ChangesLimitPopupWidget with DatePopupWidget into one common widget
-                                               changesLimitPopupWidget.$element,
-                                               datePopupWidget.$element
-                                       )
-                               }
-                       } );
-                       this.updateButtonLabel();
-
-                       // Events
-                       this.limitGroupModel.connect( this, { update: 'updateButtonLabel' } );
-                       this.daysGroupModel.connect( this, { update: 'updateButtonLabel' } );
-                       changesLimitPopupWidget.connect( this, {
-                               limit: 'onPopupLimit',
-                               groupByPage: 'onPopupGroupByPage'
-                       } );
-                       datePopupWidget.connect( this, { days: 'onPopupDays' } );
-
-                       this.$element.append( this.button.$element );
-               }
-       };
-
-       /**
-        * Respond to popup limit change event
-        *
-        * @param {string} filterName Chosen filter name
-        */
-       mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.prototype.onPopupLimit = function ( filterName ) {
-               var item = this.limitGroupModel.getItemByName( filterName );
-
-               this.controller.toggleFilterSelect( filterName, true );
-               this.controller.updateLimitDefault( item.getParamName() );
-               this.button.popup.toggle( false );
-       };
-
-       /**
-        * Respond to popup limit change event
-        *
-        * @param {boolean} isGrouped The result set is grouped by page
-        */
-       mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.prototype.onPopupGroupByPage = function ( isGrouped ) {
-               this.controller.toggleFilterSelect( this.groupByPageItemModel.getName(), isGrouped );
-               this.controller.updateGroupByPageDefault( isGrouped );
-               this.button.popup.toggle( false );
-       };
-
-       /**
-        * Respond to popup limit change event
-        *
-        * @param {string} filterName Chosen filter name
-        */
-       mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.prototype.onPopupDays = function ( filterName ) {
-               var item = this.daysGroupModel.getItemByName( filterName );
-
-               this.controller.toggleFilterSelect( filterName, true );
-               this.controller.updateDaysDefault( item.getParamName() );
-               this.button.popup.toggle( false );
-       };
-
-       /**
-        * Respond to limit choose event
-        *
-        * @param {string} filterName Filter name
-        */
-       mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.prototype.updateButtonLabel = function () {
-               var message,
-                       limit = this.limitGroupModel.findSelectedItems()[ 0 ],
-                       label = limit && limit.getLabel(),
-                       days = this.daysGroupModel.findSelectedItems()[ 0 ],
-                       daysParamName = Number( days.getParamName() ) < 1 ?
-                               'rcfilters-days-show-hours' :
-                               'rcfilters-days-show-days';
-
-               // Update the label
-               if ( label && days ) {
-                       message = mw.msg( 'rcfilters-limit-and-date-label', label,
-                               mw.msg( daysParamName, days.getLabel() )
-                       );
-                       this.button.setLabel( message );
-               }
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js
deleted file mode 100644 (file)
index 8cf9657..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-( function () {
-       /**
-        * Widget defining the popup to choose number of results
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FilterGroup} limitModel Group model for 'limit'
-        * @param {mw.rcfilters.dm.FilterItem} groupByPageItemModel Group model for 'limit'
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.ChangesLimitPopupWidget = function MwRcfiltersUiChangesLimitPopupWidget( limitModel, groupByPageItemModel, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.ChangesLimitPopupWidget.parent.call( this, config );
-
-               this.limitModel = limitModel;
-               this.groupByPageItemModel = groupByPageItemModel;
-
-               this.valuePicker = new mw.rcfilters.ui.ValuePickerWidget(
-                       this.limitModel,
-                       {
-                               label: mw.msg( 'rcfilters-limit-title' )
-                       }
-               );
-
-               this.groupByPageCheckbox = new OO.ui.CheckboxInputWidget( {
-                       selected: this.groupByPageItemModel.isSelected()
-               } );
-
-               // Events
-               this.valuePicker.connect( this, { choose: [ 'emit', 'limit' ] } );
-               this.groupByPageCheckbox.connect( this, { change: [ 'emit', 'groupByPage' ] } );
-               this.groupByPageItemModel.connect( this, { update: 'onGroupByPageModelUpdate' } );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
-                       .append(
-                               this.valuePicker.$element,
-                               new OO.ui.FieldLayout(
-                                       this.groupByPageCheckbox,
-                                       {
-                                               align: 'inline',
-                                               label: mw.msg( 'rcfilters-group-results-by-page' )
-                                       }
-                               ).$element
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.ChangesLimitPopupWidget, OO.ui.Widget );
-
-       /* Events */
-
-       /**
-        * @event limit
-        * @param {string} name Item name
-        *
-        * A limit item was chosen
-        */
-
-       /**
-        * @event groupByPage
-        * @param {boolean} isGrouped The results are grouped by page
-        *
-        * Results are grouped by page
-        */
-
-       /**
-        * Respond to group by page model update
-        */
-       mw.rcfilters.ui.ChangesLimitPopupWidget.prototype.onGroupByPageModelUpdate = function () {
-               this.groupByPageCheckbox.setSelected( this.groupByPageItemModel.isSelected() );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js
deleted file mode 100644 (file)
index b76078e..0000000
+++ /dev/null
@@ -1,385 +0,0 @@
-( function () {
-       /**
-        * List of changes
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
-        * @param {mw.rcfilters.Controller} controller
-        * @param {jQuery} $changesListRoot Root element of the changes list to attach to
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
-               filtersViewModel,
-               changesListViewModel,
-               controller,
-               $changesListRoot,
-               config
-       ) {
-               config = $.extend( {}, config, {
-                       $element: $changesListRoot
-               } );
-
-               // Parent
-               mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, config );
-
-               this.filtersViewModel = filtersViewModel;
-               this.changesListViewModel = changesListViewModel;
-               this.controller = controller;
-               this.highlightClasses = null;
-
-               // Events
-               this.filtersViewModel.connect( this, {
-                       itemUpdate: 'onItemUpdate',
-                       highlightChange: 'onHighlightChange'
-               } );
-               this.changesListViewModel.connect( this, {
-                       invalidate: 'onModelInvalidate',
-                       update: 'onModelUpdate'
-               } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget' )
-                       // We handle our own display/hide of the empty results message
-                       // We keep the timeout class here and remove it later, since at this
-                       // stage it is still needed to identify that the timeout occurred.
-                       .removeClass( 'mw-changeslist-empty' );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.Widget );
-
-       /**
-        * Get all available highlight classes
-        *
-        * @return {string[]} An array of available highlight class names
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
-               if ( !this.highlightClasses || !this.highlightClasses.length ) {
-                       this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
-                               .map( function ( filterItem ) {
-                                       return filterItem.getCssClass();
-                               } );
-               }
-
-               return this.highlightClasses;
-       };
-
-       /**
-        * Respond to the highlight feature being toggled on and off
-        *
-        * @param {boolean} highlightEnabled
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
-               if ( highlightEnabled ) {
-                       this.applyHighlight();
-               } else {
-                       this.clearHighlight();
-               }
-       };
-
-       /**
-        * Respond to a filter item model update
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onItemUpdate = function () {
-               if ( this.controller.isInitialized() && this.filtersViewModel.isHighlightEnabled() ) {
-                       // this.controller.isInitialized() is still false during page load,
-                       // we don't want to clear/apply highlights at this stage.
-                       this.clearHighlight();
-                       this.applyHighlight();
-               }
-       };
-
-       /**
-        * Respond to changes list model invalidate
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
-               $( 'body' ).addClass( 'mw-rcfilters-ui-loading' );
-       };
-
-       /**
-        * Respond to changes list model update
-        *
-        * @param {jQuery|string} $changesListContent The content of the updated changes list
-        * @param {jQuery} $fieldset The content of the updated fieldset
-        * @param {string} noResultsDetails Type of no result error
-        * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
-        * @param {boolean} from Timestamp of the new changes
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function (
-               $changesListContent, $fieldset, noResultsDetails, isInitialDOM, from
-       ) {
-               var conflictItem,
-                       $message = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
-                       isEmpty = $changesListContent === 'NO_RESULTS',
-                       // For enhanced mode, we have to load these modules, which are
-                       // not loaded for the 'regular' mode in the backend
-                       loaderPromise = mw.user.options.get( 'usenewrc' ) ?
-                               mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
-                               $.Deferred().resolve(),
-                       widget = this;
-
-               this.$element.toggleClass( 'mw-changeslist', !isEmpty );
-               if ( isEmpty ) {
-                       this.$element.empty();
-
-                       if ( this.filtersViewModel.hasConflict() ) {
-                               conflictItem = this.filtersViewModel.getFirstConflictedItem();
-
-                               $message
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-conflict' )
-                                                       .text( mw.message( 'rcfilters-noresults-conflict' ).text() ),
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-message' )
-                                                       .text( mw.message( conflictItem.getCurrentConflictResultMessage() ).text() )
-                                       );
-                       } else {
-                               $message
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-noresult' )
-                                                       .text( mw.msg( this.getMsgKeyForNoResults( noResultsDetails ) ) )
-                                       );
-
-                               // remove all classes matching mw-changeslist-*
-                               this.$element.removeClass( function ( elementIndex, allClasses ) {
-                                       return allClasses
-                                               .split( ' ' )
-                                               .filter( function ( className ) {
-                                                       return className.indexOf( 'mw-changeslist-' ) === 0;
-                                               } )
-                                               .join( ' ' );
-                               } );
-                       }
-
-                       this.$element.append( $message );
-               } else {
-                       if ( !isInitialDOM ) {
-                               this.$element.empty().append( $changesListContent );
-
-                               if ( from ) {
-                                       this.emphasizeNewChanges( from );
-                               }
-                       }
-
-                       // Apply highlight
-                       this.applyHighlight();
-
-               }
-
-               this.$element.prepend( $( '<div>' ).addClass( 'mw-changeslist-overlay' ) );
-
-               loaderPromise.done( function () {
-                       if ( !isInitialDOM && !isEmpty ) {
-                               // Make sure enhanced RC re-initializes correctly
-                               mw.hook( 'wikipage.content' ).fire( widget.$element );
-                       }
-
-                       $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
-               } );
-       };
-
-       /** Toggles overlay class on changes list
-        *
-        * @param {boolean} isVisible True if overlay should be visible
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.toggleOverlay = function ( isVisible ) {
-               this.$element.toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget--overlaid', isVisible );
-       };
-
-       /**
-        * Map a reason for having no results to its message key
-        *
-        * @param {string} reason One of the NO_RESULTS_* "constant" that represent
-        *   a reason for having no results
-        * @return {string} Key for the message that explains why there is no results in this case
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.getMsgKeyForNoResults = function ( reason ) {
-               var reasonMsgKeyMap = {
-                       NO_RESULTS_NORMAL: 'recentchanges-noresult',
-                       NO_RESULTS_TIMEOUT: 'recentchanges-timeout',
-                       NO_RESULTS_NETWORK_ERROR: 'recentchanges-network',
-                       NO_RESULTS_NO_TARGET_PAGE: 'recentchanges-notargetpage',
-                       NO_RESULTS_INVALID_TARGET_PAGE: 'allpagesbadtitle'
-               };
-               return reasonMsgKeyMap[ reason ];
-       };
-
-       /**
-        * Emphasize the elements (or groups) newer than the 'from' parameter
-        * @param {string} from Anything newer than this is considered 'new'
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
-               var $firstNew,
-                       $indicator,
-                       $newChanges = $( [] ),
-                       selector = this.inEnhancedMode() ?
-                               'table.mw-enhanced-rc[data-mw-ts]' :
-                               'li[data-mw-ts]',
-                       set = this.$element.find( selector ),
-                       length = set.length;
-
-               set.each( function ( index ) {
-                       var $this = $( this ),
-                               ts = $this.data( 'mw-ts' );
-
-                       if ( ts >= from ) {
-                               $newChanges = $newChanges.add( $this );
-                               $firstNew = $this;
-
-                               // guards against putting the marker after the last element
-                               if ( index === ( length - 1 ) ) {
-                                       $firstNew = null;
-                               }
-                       }
-               } );
-
-               if ( $firstNew ) {
-                       $indicator = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
-
-                       $firstNew.after( $indicator );
-               }
-
-               // FIXME: Use CSS transition
-               // eslint-disable-next-line jquery/no-fade
-               $newChanges
-                       .hide()
-                       .fadeIn( 1000 );
-       };
-
-       /**
-        * In enhanced mode, we need to check whether the grouped results all have the
-        * same active highlights in order to see whether the "parent" of the group should
-        * be grey or highlighted normally.
-        *
-        * This is called every time highlights are applied.
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
-               var activeHighlightClasses,
-                       $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
-
-               activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
-                       return 'mw-rcfilters-highlight-color-' + color;
-               } );
-
-               // Go over top pages and their children, and figure out if all sub-pages have the
-               // same highlights between themselves. If they do, the parent should be highlighted
-               // with all colors. If classes are different, the parent should receive a grey
-               // background
-               $enhancedTopPageCell.each( function () {
-                       var firstChildClasses, $rowsWithDifferentHighlights,
-                               $table = $( this );
-
-                       // Collect the relevant classes from the first nested child
-                       firstChildClasses = activeHighlightClasses.filter( function ( className ) {
-                               return $table.find( 'tr:nth-child(2)' ).hasClass( className );
-                       } );
-                       // Filter the non-head rows and see if they all have the same classes
-                       // to the first row
-                       $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
-                               var classesInThisRow,
-                                       $this = $( this );
-
-                               classesInThisRow = activeHighlightClasses.filter( function ( className ) {
-                                       return $this.hasClass( className );
-                               } );
-
-                               return !OO.compare( firstChildClasses, classesInThisRow );
-                       } );
-
-                       // If classes are different, tag the row for using grey color
-                       $table.find( 'tr:first-child' )
-                               .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
-               } );
-       };
-
-       /**
-        * @return {boolean} Whether the changes are grouped by page
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
-               var uri = new mw.Uri();
-               return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
-                       ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
-       };
-
-       /**
-        * Apply color classes based on filters highlight configuration
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.applyHighlight = function () {
-               if ( !this.filtersViewModel.isHighlightEnabled() ) {
-                       return;
-               }
-
-               this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
-                       var $elements = this.$element.find( '.' + filterItem.getCssClass() );
-
-                       // Add highlight class to all highlighted list items
-                       $elements
-                               .addClass(
-                                       'mw-rcfilters-highlighted ' +
-                                       'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor()
-                               );
-
-                       // Track the filters for each item in .data( 'highlightedFilters' )
-                       $elements.each( function () {
-                               var filters = $( this ).data( 'highlightedFilters' );
-                               if ( !filters ) {
-                                       filters = [];
-                                       $( this ).data( 'highlightedFilters', filters );
-                               }
-                               if ( filters.indexOf( filterItem.getLabel() ) === -1 ) {
-                                       filters.push( filterItem.getLabel() );
-                               }
-                       } );
-               }.bind( this ) );
-               // Apply a title to each highlighted item, with a list of filters
-               this.$element.find( '.mw-rcfilters-highlighted' ).each( function () {
-                       var filters = $( this ).data( 'highlightedFilters' );
-
-                       if ( filters && filters.length ) {
-                               $( this ).attr( 'title', mw.msg(
-                                       'rcfilters-highlighted-filters-list',
-                                       filters.join( mw.msg( 'comma-separator' ) )
-                               ) );
-                       }
-
-               } );
-               if ( this.inEnhancedMode() ) {
-                       this.updateEnhancedParentHighlight();
-               }
-
-               // Turn on highlights
-               this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
-       };
-
-       /**
-        * Remove all color classes
-        */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.clearHighlight = function () {
-               // Remove highlight classes
-               mw.rcfilters.HighlightColors.forEach( function ( color ) {
-                       this.$element
-                               .find( '.mw-rcfilters-highlight-color-' + color )
-                               .removeClass( 'mw-rcfilters-highlight-color-' + color );
-               }.bind( this ) );
-
-               this.$element.find( '.mw-rcfilters-highlighted' )
-                       .removeAttr( 'title' )
-                       .removeData( 'highlightedFilters' )
-                       .removeClass( 'mw-rcfilters-highlighted' );
-
-               // Remove grey from enhanced rows
-               this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
-                       .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
-
-               // Turn off highlights
-               this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js
deleted file mode 100644 (file)
index b273a01..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-( function () {
-       /**
-        * A widget representing a single toggle filter
-        *
-        * @extends OO.ui.CheckboxInputWidget
-        *
-        * @constructor
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.CheckboxInputWidget = function MwRcfiltersUiCheckboxInputWidget( config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.CheckboxInputWidget.parent.call( this, config );
-
-               // Event
-               this.$input
-                       // HACK: This widget just pretends to be a checkbox for visual purposes.
-                       // In reality, all actions - setting to true or false, etc - are
-                       // decided by the model, and executed by the controller. This means
-                       // that we want to let the controller and model make the decision
-                       // of whether to check/uncheck this checkboxInputWidget, and for that,
-                       // we have to bypass the browser action that checks/unchecks it during
-                       // click.
-                       .on( 'click', false )
-                       .on( 'change', this.onUserChange.bind( this ) );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.CheckboxInputWidget, OO.ui.CheckboxInputWidget );
-
-       /* Events */
-
-       /**
-        * @event userChange
-        * @param {boolean} Current state of the checkbox
-        *
-        * The user has checked or unchecked this checkbox
-        */
-
-       /* Methods */
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.CheckboxInputWidget.prototype.onEdit = function () {
-               // Similarly to preventing defaults in 'click' event, we want
-               // to prevent this widget from deciding anything about its own
-               // state; it emits a change event and the model and controller
-               // make a decision about what its select state is.
-               // onEdit has a widget.$input.prop( 'checked' ) inside a setTimeout()
-               // so we really want to prevent that from messing with what
-               // the model decides the state of the widget is.
-       };
-
-       /**
-        * Respond to checkbox change by a user and emit 'userChange'.
-        */
-       mw.rcfilters.ui.CheckboxInputWidget.prototype.onUserChange = function () {
-               this.emit( 'userChange', this.$input.prop( 'checked' ) );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js
deleted file mode 100644 (file)
index 792ea4b..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-( function () {
-       /**
-        * Widget defining the popup to choose date for the results
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'days'
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.DatePopupWidget = function MwRcfiltersUiDatePopupWidget( model, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.ChangesLimitPopupWidget.parent.call( this, config );
-               // Mixin constructors
-               OO.ui.mixin.LabelElement.call( this, config );
-
-               this.model = model;
-
-               this.hoursValuePicker = new mw.rcfilters.ui.ValuePickerWidget(
-                       this.model,
-                       {
-                               classes: [ 'mw-rcfilters-ui-datePopupWidget-hours' ],
-                               label: mw.msg( 'rcfilters-hours-title' ),
-                               itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) < 1; }
-                       }
-               );
-               this.daysValuePicker = new mw.rcfilters.ui.ValuePickerWidget(
-                       this.model,
-                       {
-                               classes: [ 'mw-rcfilters-ui-datePopupWidget-days' ],
-                               label: mw.msg( 'rcfilters-days-title' ),
-                               itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) >= 1; }
-                       }
-               );
-
-               // Events
-               this.hoursValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
-               this.daysValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-datePopupWidget' )
-                       .append(
-                               this.$label
-                                       .addClass( 'mw-rcfilters-ui-datePopupWidget-title' ),
-                               this.hoursValuePicker.$element,
-                               this.daysValuePicker.$element
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.DatePopupWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.DatePopupWidget, OO.ui.mixin.LabelElement );
-
-       /* Events */
-
-       /**
-        * @event days
-        * @param {string} name Item name
-        *
-        * A days item was chosen
-        */
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js
deleted file mode 100644 (file)
index 289f1ee..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-( function () {
-       /**
-        * A button to configure highlight for a filter item
-        *
-        * @extends OO.ui.PopupButtonWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {mw.rcfilters.dm.FilterItem} model Filter item model
-        * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.FilterItemHighlightButton = function MwRcfiltersUiFilterItemHighlightButton( controller, model, highlightPopup, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( true, {}, config, {
-                       icon: 'highlight',
-                       indicator: 'down'
-               } ) );
-
-               this.controller = controller;
-               this.model = model;
-               this.popup = highlightPopup;
-
-               // Event
-               this.model.connect( this, { update: 'updateUiBasedOnModel' } );
-               // This lives inside a MenuOptionWidget, which intercepts mousedown
-               // to select the item. We want to prevent that when we click the highlight
-               // button
-               this.$element.on( 'mousedown', function ( e ) {
-                       e.stopPropagation();
-               } );
-
-               this.updateUiBasedOnModel();
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterItemHighlightButton, OO.ui.PopupButtonWidget );
-
-       /* Static Properties */
-
-       /**
-        * @static
-        */
-       mw.rcfilters.ui.FilterItemHighlightButton.static.cancelButtonMouseDownEvents = true;
-
-       /* Methods */
-
-       mw.rcfilters.ui.FilterItemHighlightButton.prototype.onAction = function () {
-               this.popup.setAssociatedButton( this );
-               this.popup.setFilterItem( this.model );
-
-               // Parent method
-               mw.rcfilters.ui.FilterItemHighlightButton.parent.prototype.onAction.call( this );
-       };
-
-       /**
-        * Respond to item model update event
-        */
-       mw.rcfilters.ui.FilterItemHighlightButton.prototype.updateUiBasedOnModel = function () {
-               var currentColor = this.model.getHighlightColor(),
-                       widget = this;
-
-               this.$icon.toggleClass(
-                       'mw-rcfilters-ui-filterItemHighlightButton-circle',
-                       currentColor !== null
-               );
-
-               mw.rcfilters.HighlightColors.forEach( function ( c ) {
-                       widget.$icon
-                               .toggleClass(
-                                       'mw-rcfilters-ui-filterItemHighlightButton-circle-color-' + c,
-                                       c === currentColor
-                               );
-               } );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js
deleted file mode 100644 (file)
index 1fef7a0..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-( function () {
-       /**
-        * Menu header for the RCFilters filters menu
-        *
-        * @class
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget = function MwRcfiltersUiFilterMenuHeaderWidget( controller, model, config ) {
-               config = config || {};
-
-               this.controller = controller;
-               this.model = model;
-               this.$overlay = config.$overlay || this.$element;
-
-               // Parent
-               mw.rcfilters.ui.FilterMenuHeaderWidget.parent.call( this, config );
-               OO.ui.mixin.LabelElement.call( this, $.extend( {
-                       label: mw.msg( 'rcfilters-filterlist-title' ),
-                       $label: $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-title' )
-               }, config ) );
-
-               // "Back" to default view button
-               this.backButton = new OO.ui.ButtonWidget( {
-                       icon: 'previous',
-                       framed: false,
-                       title: mw.msg( 'rcfilters-view-return-to-default-tooltip' ),
-                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-backButton' ]
-               } );
-               this.backButton.toggle( this.model.getCurrentView() !== 'default' );
-
-               // Help icon for Tagged edits
-               this.helpIcon = new OO.ui.ButtonWidget( {
-                       icon: 'helpNotice',
-                       framed: false,
-                       title: mw.msg( 'rcfilters-view-tags-help-icon-tooltip' ),
-                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-helpIcon' ],
-                       href: mw.util.getUrl( 'Special:Tags' ),
-                       target: '_blank'
-               } );
-               this.helpIcon.toggle( this.model.getCurrentView() === 'tags' );
-
-               // Highlight button
-               this.highlightButton = new OO.ui.ToggleButtonWidget( {
-                       icon: 'highlight',
-                       label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
-                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-hightlightButton' ]
-               } );
-
-               // Invert namespaces button
-               this.invertNamespacesButton = new OO.ui.ToggleButtonWidget( {
-                       icon: '',
-                       classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-invertNamespacesButton' ]
-               } );
-               this.invertNamespacesButton.toggle( this.model.getCurrentView() === 'namespaces' );
-
-               // Events
-               this.backButton.connect( this, { click: 'onBackButtonClick' } );
-               this.highlightButton
-                       .connect( this, { click: 'onHighlightButtonClick' } );
-               this.invertNamespacesButton
-                       .connect( this, { click: 'onInvertNamespacesButtonClick' } );
-               this.model.connect( this, {
-                       highlightChange: 'onModelHighlightChange',
-                       searchChange: 'onModelSearchChange',
-                       initialize: 'onModelInitialize'
-               } );
-               this.view = this.model.getCurrentView();
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-back' )
-                                                                       .append( this.backButton.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-title' )
-                                                                       .append( this.$label, this.helpIcon.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-invert' )
-                                                                       .append( this.invertNamespacesButton.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-filterMenuHeaderWidget-header-highlight' )
-                                                                       .append( this.highlightButton.$element )
-                                                       )
-                                       )
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterMenuHeaderWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.FilterMenuHeaderWidget, OO.ui.mixin.LabelElement );
-
-       /* Methods */
-
-       /**
-        * Respond to model initialization event
-        *
-        * Note: need to wait for initialization before getting the invertModel
-        * and registering its update event. Creating all the models before the UI
-        * would help with that.
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelInitialize = function () {
-               this.invertModel = this.model.getInvertModel();
-               this.updateInvertButton();
-               this.invertModel.connect( this, { update: 'updateInvertButton' } );
-       };
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelSearchChange = function () {
-               var currentView = this.model.getCurrentView();
-
-               if ( this.view !== currentView ) {
-                       this.setLabel( this.model.getViewTitle( currentView ) );
-
-                       this.invertNamespacesButton.toggle( currentView === 'namespaces' );
-                       this.backButton.toggle( currentView !== 'default' );
-                       this.helpIcon.toggle( currentView === 'tags' );
-                       this.view = currentView;
-               }
-       };
-
-       /**
-        * Respond to model highlight change event
-        *
-        * @param {boolean} highlightEnabled Highlight is enabled
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) {
-               this.highlightButton.setActive( highlightEnabled );
-       };
-
-       /**
-        * Update the state of the invert button
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.updateInvertButton = function () {
-               this.invertNamespacesButton.setActive( this.invertModel.isSelected() );
-               this.invertNamespacesButton.setLabel(
-                       this.invertModel.isSelected() ?
-                               mw.msg( 'rcfilters-exclude-button-on' ) :
-                               mw.msg( 'rcfilters-exclude-button-off' )
-               );
-       };
-
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onBackButtonClick = function () {
-               this.controller.switchView( 'default' );
-       };
-
-       /**
-        * Respond to highlight button click
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onHighlightButtonClick = function () {
-               this.controller.toggleHighlight();
-       };
-
-       /**
-        * Respond to highlight button click
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onInvertNamespacesButtonClick = function () {
-               this.controller.toggleInvertedNamespaces();
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js
deleted file mode 100644 (file)
index 8840155..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-( function () {
-       /**
-        * A widget representing a single toggle filter
-        *
-        * @extends mw.rcfilters.ui.ItemMenuOptionWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
-        * @param {mw.rcfilters.dm.FilterItem} invertModel
-        * @param {mw.rcfilters.dm.FilterItem} itemModel Filter item model
-        * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker popup
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget(
-               controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
-       ) {
-               config = config || {};
-
-               this.controller = controller;
-               this.invertModel = invertModel;
-               this.model = itemModel;
-
-               // Parent
-               mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, controller, filtersViewModel, this.invertModel, itemModel, highlightPopup, config );
-
-               // Event
-               this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterMenuOptionWidget' );
-       };
-
-       /* Initialization */
-       OO.inheritClass( mw.rcfilters.ui.FilterMenuOptionWidget, mw.rcfilters.ui.ItemMenuOptionWidget );
-
-       /* Static properties */
-
-       // We do our own scrolling to top
-       mw.rcfilters.ui.FilterMenuOptionWidget.static.scrollIntoViewOnSelect = false;
-
-       /* Methods */
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.updateUiBasedOnState = function () {
-               // Parent
-               mw.rcfilters.ui.FilterMenuOptionWidget.parent.prototype.updateUiBasedOnState.call( this );
-
-               this.setCurrentMuteState();
-       };
-
-       /**
-        * Respond to item group model update event
-        */
-       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.onGroupModelUpdate = function () {
-               this.setCurrentMuteState();
-       };
-
-       /**
-        * Set the current muted view of the widget based on its state
-        */
-       mw.rcfilters.ui.FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
-               if (
-                       this.model.getGroupModel().getView() === 'namespaces' &&
-                       this.invertModel.isSelected()
-               ) {
-                       // This is an inverted behavior than the other rules, specifically
-                       // for inverted namespaces
-                       this.setFlags( {
-                               muted: this.model.isSelected()
-                       } );
-               } else {
-                       this.setFlags( {
-                               muted: (
-                                       this.model.isConflicted() ||
-                                       (
-                                               // Item is also muted when any of the items in its group is active
-                                               this.model.getGroupModel().isActive() &&
-                                               // But it isn't selected
-                                               !this.model.isSelected() &&
-                                               // And also not included
-                                               !this.model.isIncluded()
-                                       )
-                               )
-                       } );
-               }
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js
deleted file mode 100644 (file)
index 3d598c9..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-( function () {
-       /**
-        * A widget representing a menu section for filter groups
-        *
-        * @class
-        * @extends OO.ui.MenuSectionOptionWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {mw.rcfilters.dm.FilterGroup} model Filter group model
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} [$overlay] Overlay
-        */
-       mw.rcfilters.ui.FilterMenuSectionOptionWidget = function MwRcfiltersUiFilterMenuSectionOptionWidget( controller, model, config ) {
-               var whatsThisMessages,
-                       $header = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header' ),
-                       $popupContent = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content' );
-
-               config = config || {};
-
-               this.controller = controller;
-               this.model = model;
-               this.$overlay = config.$overlay || this.$element;
-
-               // Parent
-               mw.rcfilters.ui.FilterMenuSectionOptionWidget.parent.call( this, $.extend( {
-                       label: this.model.getTitle(),
-                       $label: $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-header-title' )
-               }, config ) );
-
-               $header.append( this.$label );
-
-               if ( this.model.hasWhatsThis() ) {
-                       whatsThisMessages = this.model.getWhatsThis();
-
-                       // Create popup
-                       if ( whatsThisMessages.header ) {
-                               $popupContent.append(
-                                       ( new OO.ui.LabelWidget( {
-                                               label: mw.msg( whatsThisMessages.header ),
-                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-header' ]
-                                       } ) ).$element
-                               );
-                       }
-                       if ( whatsThisMessages.body ) {
-                               $popupContent.append(
-                                       ( new OO.ui.LabelWidget( {
-                                               label: mw.msg( whatsThisMessages.body ),
-                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-body' ]
-                                       } ) ).$element
-                               );
-                       }
-                       if ( whatsThisMessages.linkText && whatsThisMessages.url ) {
-                               $popupContent.append(
-                                       ( new OO.ui.ButtonWidget( {
-                                               framed: false,
-                                               flags: [ 'progressive' ],
-                                               href: whatsThisMessages.url,
-                                               label: mw.msg( whatsThisMessages.linkText ),
-                                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup-content-link' ]
-                                       } ) ).$element
-                               );
-                       }
-
-                       // Add button
-                       this.whatsThisButton = new OO.ui.PopupButtonWidget( {
-                               framed: false,
-                               label: mw.msg( 'rcfilters-filterlist-whatsthis' ),
-                               $overlay: this.$overlay,
-                               classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
-                               flags: [ 'progressive' ],
-                               popup: {
-                                       padded: false,
-                                       align: 'center',
-                                       position: 'above',
-                                       $content: $popupContent,
-                                       classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton-popup' ]
-                               }
-                       } );
-
-                       $header
-                               .append( this.whatsThisButton.$element );
-               }
-
-               // Events
-               this.model.connect( this, { update: 'updateUiBasedOnState' } );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget' )
-                       .addClass( 'mw-rcfilters-ui-filterMenuSectionOptionWidget-name-' + this.model.getName() )
-                       .append( $header );
-               this.updateUiBasedOnState();
-       };
-
-       /* Initialize */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterMenuSectionOptionWidget, OO.ui.MenuSectionOptionWidget );
-
-       /* Methods */
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.FilterMenuSectionOptionWidget.prototype.updateUiBasedOnState = function () {
-               this.$element.toggleClass(
-                       'mw-rcfilters-ui-filterMenuSectionOptionWidget-active',
-                       this.model.isActive()
-               );
-               this.toggle( this.model.isVisible() );
-       };
-
-       /**
-        * Get the group name
-        *
-        * @return {string} Group name
-        */
-       mw.rcfilters.ui.FilterMenuSectionOptionWidget.prototype.getName = function () {
-               return this.model.getName();
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js
deleted file mode 100644 (file)
index 411ada9..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-( function () {
-       /**
-        * Extend OOUI's FilterTagItemWidget to also display a popup on hover.
-        *
-        * @class
-        * @extends mw.rcfilters.ui.TagItemWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
-        * @param {mw.rcfilters.dm.FilterItem} invertModel
-        * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget(
-               controller, filtersViewModel, invertModel, itemModel, config
-       ) {
-               config = config || {};
-
-               mw.rcfilters.ui.FilterTagItemWidget.parent.call( this, controller, filtersViewModel, invertModel, itemModel, config );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterTagItemWidget' );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterTagItemWidget, mw.rcfilters.ui.TagItemWidget );
-
-       /* Methods */
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagItemWidget.prototype.setCurrentMuteState = function () {
-               this.setFlags( {
-                       muted: (
-                               !this.itemModel.isSelected() ||
-                               this.itemModel.isIncluded() ||
-                               this.itemModel.isFullyCovered()
-                       ),
-                       invalid: this.itemModel.isSelected() && this.itemModel.isConflicted()
-               } );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
deleted file mode 100644 (file)
index 6d45144..0000000
+++ /dev/null
@@ -1,770 +0,0 @@
-( function () {
-       /**
-        * List displaying all filter groups
-        *
-        * @class
-        * @extends OO.ui.MenuTagMultiselectWidget
-        * @mixins OO.ui.mixin.PendingElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
-        *  system. If not given, falls back to this widget's $element
-        * @cfg {boolean} [collapsed] Filter area is collapsed
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
-               var rcFiltersRow,
-                       title = new OO.ui.LabelWidget( {
-                               label: mw.msg( 'rcfilters-activefilters' ),
-                               classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
-                       } ),
-                       $contentWrapper = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
-
-               config = config || {};
-
-               this.controller = controller;
-               this.model = model;
-               this.queriesModel = savedQueriesModel;
-               this.$overlay = config.$overlay || this.$element;
-               this.$wrapper = config.$wrapper || this.$element;
-               this.matchingQuery = null;
-               this.currentView = this.model.getCurrentView();
-               this.collapsed = false;
-
-               // Parent
-               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
-                       label: mw.msg( 'rcfilters-filterlist-title' ),
-                       placeholder: mw.msg( 'rcfilters-empty-filter' ),
-                       inputPosition: 'outline',
-                       allowArbitrary: false,
-                       allowDisplayInvalidTags: false,
-                       allowReordering: false,
-                       $overlay: this.$overlay,
-                       menu: {
-                               // Our filtering is done through the model
-                               filterFromInput: false,
-                               hideWhenOutOfView: false,
-                               hideOnChoose: false,
-                               width: 650,
-                               footers: [
-                                       {
-                                               name: 'viewSelect',
-                                               sticky: false,
-                                               // View select menu, appears on default view only
-                                               $element: $( '<div>' )
-                                                       .append( new mw.rcfilters.ui.ViewSwitchWidget( this.controller, this.model ).$element ),
-                                               views: [ 'default' ]
-                                       },
-                                       {
-                                               name: 'feedback',
-                                               // Feedback footer, appears on all views
-                                               $element: $( '<div>' )
-                                                       .append(
-                                                               new OO.ui.ButtonWidget( {
-                                                                       framed: false,
-                                                                       icon: 'feedback',
-                                                                       flags: [ 'progressive' ],
-                                                                       label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
-                                                                       href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
-                                                               } ).$element
-                                                       )
-                                       }
-                               ]
-                       },
-                       input: {
-                               icon: 'menu',
-                               placeholder: mw.msg( 'rcfilters-search-placeholder' )
-                       }
-               }, config ) );
-
-               this.savedQueryTitle = new OO.ui.LabelWidget( {
-                       label: '',
-                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
-               } );
-
-               this.resetButton = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
-               } );
-
-               this.hideShowButton = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       flags: [ 'progressive' ],
-                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
-               } );
-               this.toggleCollapsed( !!config.collapsed );
-
-               if ( !mw.user.isAnon() ) {
-                       this.saveQueryButton = new mw.rcfilters.ui.SaveFiltersPopupButtonWidget(
-                               this.controller,
-                               this.queriesModel,
-                               {
-                                       $overlay: this.$overlay
-                               }
-                       );
-
-                       this.saveQueryButton.$element.on( 'mousedown', function ( e ) {
-                               e.stopPropagation();
-                       } );
-
-                       this.saveQueryButton.connect( this, {
-                               click: 'onSaveQueryButtonClick',
-                               saveCurrent: 'setSavedQueryVisibility'
-                       } );
-                       this.queriesModel.connect( this, {
-                               itemUpdate: 'onSavedQueriesItemUpdate',
-                               initialize: 'onSavedQueriesInitialize',
-                               default: 'reevaluateResetRestoreState'
-                       } );
-               }
-
-               this.emptyFilterMessage = new OO.ui.LabelWidget( {
-                       label: mw.msg( 'rcfilters-empty-filter' ),
-                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
-               } );
-               this.$content.append( this.emptyFilterMessage.$element );
-
-               // Events
-               this.resetButton.connect( this, { click: 'onResetButtonClick' } );
-               this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
-               // Stop propagation for mousedown, so that the widget doesn't
-               // trigger the focus on the input and scrolls up when we click the reset button
-               this.resetButton.$element.on( 'mousedown', function ( e ) {
-                       e.stopPropagation();
-               } );
-               this.hideShowButton.$element.on( 'mousedown', function ( e ) {
-                       e.stopPropagation();
-               } );
-               this.model.connect( this, {
-                       initialize: 'onModelInitialize',
-                       update: 'onModelUpdate',
-                       searchChange: 'onModelSearchChange',
-                       itemUpdate: 'onModelItemUpdate',
-                       highlightChange: 'onModelHighlightChange'
-               } );
-               this.input.connect( this, { change: 'onInputChange' } );
-
-               // The filter list and button should appear side by side regardless of how
-               // wide the button is; the button also changes its width depending
-               // on language and its state, so the safest way to present both side
-               // by side is with a table layout
-               rcFiltersRow = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-row' )
-                       .append(
-                               this.$content
-                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
-                       );
-
-               if ( !mw.user.isAnon() ) {
-                       rcFiltersRow.append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
-                                       .append( this.saveQueryButton.$element )
-                       );
-               }
-
-               // Add a selector at the right of the input
-               this.viewsSelectWidget = new OO.ui.ButtonSelectWidget( {
-                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select-widget' ],
-                       items: [
-                               new OO.ui.ButtonOptionWidget( {
-                                       framed: false,
-                                       data: 'namespaces',
-                                       icon: 'article',
-                                       label: mw.msg( 'namespaces' ),
-                                       title: mw.msg( 'rcfilters-view-namespaces-tooltip' )
-                               } ),
-                               new OO.ui.ButtonOptionWidget( {
-                                       framed: false,
-                                       data: 'tags',
-                                       icon: 'tag',
-                                       label: mw.msg( 'tags-title' ),
-                                       title: mw.msg( 'rcfilters-view-tags-tooltip' )
-                               } )
-                       ]
-               } );
-
-               // Rearrange the UI so the select widget is at the right of the input
-               this.$element.append(
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-table' )
-                               .append(
-                                       $( '<div>' )
-                                               .addClass( 'mw-rcfilters-ui-row' )
-                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
-                                               .append(
-                                                       $( '<div>' )
-                                                               .addClass( 'mw-rcfilters-ui-cell' )
-                                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-input' )
-                                                               .append( this.input.$element ),
-                                                       $( '<div>' )
-                                                               .addClass( 'mw-rcfilters-ui-cell' )
-                                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views-select' )
-                                                               .append( this.viewsSelectWidget.$element )
-                                               )
-                               )
-               );
-
-               // Event
-               this.viewsSelectWidget.connect( this, { choose: 'onViewsSelectWidgetChoose' } );
-
-               rcFiltersRow.append(
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-cell' )
-                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
-                               .append( this.resetButton.$element )
-               );
-
-               // Build the content
-               $contentWrapper.append(
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
-                               .append(
-                                       $( '<div>' )
-                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-title' )
-                                               .append( title.$element ),
-                                       $( '<div>' )
-                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-queryName' )
-                                               .append( this.savedQueryTitle.$element ),
-                                       $( '<div>' )
-                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top-hideshow' )
-                                               .append(
-                                                       this.hideShowButton.$element
-                                               )
-                               ),
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-table' )
-                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
-                               .append( rcFiltersRow )
-               );
-
-               // Initialize
-               this.$handle.append( $contentWrapper );
-               this.emptyFilterMessage.toggle( this.isEmpty() );
-               this.savedQueryTitle.toggle( false );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
-
-               this.reevaluateResetRestoreState();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
-
-       /* Methods */
-
-       /**
-        * Override parent method to avoid unnecessary resize events.
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.updateIfHeightChanged = function () { };
-
-       /**
-        * Respond to view select widget choose event
-        *
-        * @param {OO.ui.ButtonOptionWidget} buttonOptionWidget Chosen widget
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onViewsSelectWidgetChoose = function ( buttonOptionWidget ) {
-               this.controller.switchView( buttonOptionWidget.getData() );
-               this.viewsSelectWidget.selectItem( null );
-               this.focus();
-       };
-
-       /**
-        * Respond to model search change event
-        *
-        * @param {string} value Search value
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelSearchChange = function ( value ) {
-               this.input.setValue( value );
-       };
-
-       /**
-        * Respond to input change event
-        *
-        * @param {string} value Value of the input
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
-               this.controller.setSearch( value );
-       };
-
-       /**
-        * Respond to query button click
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
-               this.getMenu().toggle( false );
-       };
-
-       /**
-        * Respond to save query model initialization
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSavedQueriesInitialize = function () {
-               this.setSavedQueryVisibility();
-       };
-
-       /**
-        * Respond to save query item change. Mainly this is done to update the label in case
-        * a query item has been edited
-        *
-        * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
-               if ( this.matchingQuery === item ) {
-                       // This means we just edited the item that is currently matched
-                       this.savedQueryTitle.setLabel( item.getLabel() );
-               }
-       };
-
-       /**
-        * Respond to menu toggle
-        *
-        * @param {boolean} isVisible Menu is visible
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
-               // Parent
-               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
-
-               if ( isVisible ) {
-                       this.focus();
-
-                       mw.hook( 'RcFilters.popup.open' ).fire();
-
-                       if ( !this.getMenu().findSelectedItem() ) {
-                               // If there are no selected items, scroll menu to top
-                               // This has to be in a setTimeout so the menu has time
-                               // to be positioned and fixed
-                               setTimeout(
-                                       function () {
-                                               this.getMenu().scrollToTop();
-                                       }.bind( this )
-                               );
-                       }
-               } else {
-                       // Clear selection
-                       this.selectTag( null );
-
-                       // Clear the search
-                       this.controller.setSearch( '' );
-
-                       // Log filter grouping
-                       this.controller.trackFilterGroupings( 'filtermenu' );
-
-                       this.blur();
-               }
-
-               this.input.setIcon( isVisible ? 'search' : 'menu' );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () {
-               // Parent
-               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
-
-               // Only scroll to top of the viewport if:
-               // - The widget is more than 20px from the top
-               // - The widget is not above the top of the viewport (do not scroll downwards)
-               //   (This isn't represented because >20 is, anyways and always, bigger than 0)
-               this.scrollToTop( this.$element, 0, { min: 20, max: Infinity } );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.doInputEscape = function () {
-               // Parent
-               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
-
-               // Blur the input
-               this.input.$input.trigger( 'blur' );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
-               if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
-                       this.menu.toggle();
-
-                       return false;
-               }
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () {
-               // If initialized, call parent method.
-               if ( this.controller.isInitialized() ) {
-                       mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
-               }
-
-               this.emptyFilterMessage.toggle( this.isEmpty() );
-       };
-
-       /**
-        * Respond to model initialize event
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
-               this.setSavedQueryVisibility();
-       };
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
-               this.updateElementsForView();
-       };
-
-       /**
-        * Update the elements in the widget to the current view
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
-               var view = this.model.getCurrentView(),
-                       inputValue = this.input.getValue().trim(),
-                       inputView = this.model.getViewByTrigger( inputValue.substr( 0, 1 ) );
-
-               if ( inputView !== 'default' ) {
-                       // We have a prefix already, remove it
-                       inputValue = inputValue.substr( 1 );
-               }
-
-               if ( inputView !== view ) {
-                       // Add the correct prefix
-                       inputValue = this.model.getViewTrigger( view ) + inputValue;
-               }
-
-               // Update input
-               this.input.setValue( inputValue );
-
-               if ( this.currentView !== view ) {
-                       this.scrollToTop( this.$element );
-                       this.currentView = view;
-               }
-       };
-
-       /**
-        * Set the visibility of the saved query button
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
-               if ( mw.user.isAnon() ) {
-                       return;
-               }
-
-               this.matchingQuery = this.controller.findQueryMatchingCurrentState();
-
-               this.savedQueryTitle.setLabel(
-                       this.matchingQuery ? this.matchingQuery.getLabel() : ''
-               );
-               this.savedQueryTitle.toggle( !!this.matchingQuery );
-               this.saveQueryButton.setDisabled( !!this.matchingQuery );
-               this.saveQueryButton.setTitle( !this.matchingQuery ?
-                       mw.msg( 'rcfilters-savedqueries-add-new-title' ) :
-                       mw.msg( 'rcfilters-savedqueries-already-saved' ) );
-
-               if ( this.matchingQuery ) {
-                       this.emphasize();
-               }
-       };
-
-       /**
-        * Respond to model itemUpdate event
-        * fixme: when a new state is applied to the model this function is called 60+ times in a row
-        *
-        * @param {mw.rcfilters.dm.FilterItem} item Filter item model
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
-               if ( !item.getGroupModel().isHidden() ) {
-                       if (
-                               item.isSelected() ||
-                               (
-                                       this.model.isHighlightEnabled() &&
-                                       item.getHighlightColor()
-                               )
-                       ) {
-                               this.addTag( item.getName(), item.getLabel() );
-                       } else {
-                               // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
-                               if ( this.findItemFromData( item.getName() ) !== null ) {
-                                       this.removeTagByData( item.getName() );
-                               }
-                       }
-               }
-
-               this.setSavedQueryVisibility();
-
-               // Re-evaluate reset state
-               this.reevaluateResetRestoreState();
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
-               return (
-                       this.model.getItemByName( data ) &&
-                       !this.isDuplicateData( data )
-               );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
-               this.controller.toggleFilterSelect( item.model.getName() );
-
-               // Select the tag if it exists, or reset selection otherwise
-               this.selectTag( this.findItemFromData( item.model.getName() ) );
-
-               this.focus();
-       };
-
-       /**
-        * Respond to highlightChange event
-        *
-        * @param {boolean} isHighlightEnabled Highlight is enabled
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
-               var highlightedItems = this.model.getHighlightedItems();
-
-               if ( isHighlightEnabled ) {
-                       // Add capsule widgets
-                       highlightedItems.forEach( function ( filterItem ) {
-                               this.addTag( filterItem.getName(), filterItem.getLabel() );
-                       }.bind( this ) );
-               } else {
-                       // Remove capsule widgets if they're not selected
-                       highlightedItems.forEach( function ( filterItem ) {
-                               if ( !filterItem.isSelected() ) {
-                                       // Only attempt to remove the tag if we can find an item for it (T198140, T198231)
-                                       if ( this.findItemFromData( filterItem.getName() ) !== null ) {
-                                               this.removeTagByData( filterItem.getName() );
-                                       }
-                               }
-                       }.bind( this ) );
-               }
-
-               this.setSavedQueryVisibility();
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
-               var menuOption = this.menu.getItemFromModel( tagItem.getModel() );
-
-               this.menu.setUserSelecting( true );
-               // Parent method
-               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
-
-               // Switch view
-               this.controller.resetSearchForView( tagItem.getView() );
-
-               this.selectTag( tagItem );
-               this.scrollToTop( menuOption.$element );
-
-               this.menu.setUserSelecting( false );
-       };
-
-       /**
-        * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
-        * If no items are given, reset selection from all.
-        *
-        * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
-        *  omit to deselect all
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
-               var i, len, selected;
-
-               for ( i = 0, len = this.items.length; i < len; i++ ) {
-                       selected = this.items[ i ] === item;
-                       if ( this.items[ i ].isSelected() !== selected ) {
-                               this.items[ i ].toggleSelected( selected );
-                       }
-               }
-       };
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
-               // Parent method
-               mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
-
-               this.controller.clearFilter( tagItem.getName() );
-
-               tagItem.destroy();
-       };
-
-       /**
-        * Respond to click event on the reset button
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
-               if ( this.model.areVisibleFiltersEmpty() ) {
-                       // Reset to default filters
-                       this.controller.resetToDefaults();
-               } else {
-                       // Reset to have no filters
-                       this.controller.emptyFilters();
-               }
-       };
-
-       /**
-        * Respond to hide/show button click
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
-               this.toggleCollapsed();
-       };
-
-       /**
-        * Toggle the collapsed state of the filters widget
-        *
-        * @param {boolean} isCollapsed Widget is collapsed
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
-               isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
-
-               this.collapsed = isCollapsed;
-
-               if ( isCollapsed ) {
-                       // If we are collapsing, close the menu, in case it was open
-                       // We should make sure the menu closes before the rest of the elements
-                       // are hidden, otherwise there is an unknown error in jQuery as ooui
-                       // sets and unsets properties on the input (which is hidden at that point)
-                       this.menu.toggle( false );
-               }
-               this.input.setDisabled( isCollapsed );
-               this.hideShowButton.setLabel( mw.msg(
-                       isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
-               ) );
-               this.hideShowButton.setTitle( mw.msg(
-                       isCollapsed ? 'rcfilters-activefilters-show-tooltip' : 'rcfilters-activefilters-hide-tooltip'
-               ) );
-
-               // Toggle the wrapper class, so we have min height values correctly throughout
-               this.$wrapper.toggleClass( 'mw-rcfilters-collapsed', isCollapsed );
-
-               // Save the state
-               this.controller.updateCollapsedState( isCollapsed );
-       };
-
-       /**
-        * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
-               var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
-                       currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
-                       hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
-
-               this.resetButton.setIcon(
-                       currFiltersAreEmpty ? 'history' : 'trash'
-               );
-
-               this.resetButton.setLabel(
-                       currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
-               );
-               this.resetButton.setTitle(
-                       currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
-               );
-
-               this.resetButton.toggle( !hideResetButton );
-               this.emptyFilterMessage.toggle( currFiltersAreEmpty );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
-               return new mw.rcfilters.ui.MenuSelectWidget(
-                       this.controller,
-                       this.model,
-                       menuConfig
-               );
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
-               var filterItem = this.model.getItemByName( data );
-
-               if ( filterItem ) {
-                       return new mw.rcfilters.ui.FilterTagItemWidget(
-                               this.controller,
-                               this.model,
-                               this.model.getInvertModel(),
-                               filterItem,
-                               {
-                                       $overlay: this.$overlay
-                               }
-                       );
-               }
-       };
-
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.emphasize = function () {
-               if (
-                       !this.$handle.hasClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' )
-               ) {
-                       this.$handle
-                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' )
-                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
-
-                       setTimeout( function () {
-                               this.$handle
-                                       .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-emphasize' );
-
-                               setTimeout( function () {
-                                       this.$handle
-                                               .removeClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-animate' );
-                               }.bind( this ), 1000 );
-                       }.bind( this ), 500 );
-
-               }
-       };
-       /**
-        * Scroll the element to top within its container
-        *
-        * @private
-        * @param {jQuery} $element Element to position
-        * @param {number} [marginFromTop=0] When scrolling the entire widget to the top, leave this
-        *  much space (in pixels) above the widget.
-        * @param {Object} [threshold] Minimum distance from the top of the element to scroll at all
-        * @param {number} [threshold.min] Minimum distance above the element
-        * @param {number} [threshold.max] Minimum distance below the element
-        */
-       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop, threshold ) {
-               var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
-                       pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
-                       containerScrollTop = $( container ).scrollTop(),
-                       effectiveScrollTop = $( container ).is( 'body, html' ) ? 0 : containerScrollTop,
-                       newScrollTop = effectiveScrollTop + pos.top - ( marginFromTop || 0 );
-
-               // Scroll to item
-               if (
-                       threshold === undefined ||
-                       (
-                               (
-                                       threshold.min === undefined ||
-                                       newScrollTop - containerScrollTop >= threshold.min
-                               ) &&
-                               (
-                                       threshold.max === undefined ||
-                                       newScrollTop - containerScrollTop <= threshold.max
-                               )
-                       )
-               ) {
-                       // eslint-disable-next-line jquery/no-animate
-                       $( container ).animate( {
-                               scrollTop: newScrollTop
-                       } );
-               }
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
deleted file mode 100644 (file)
index 567d86d..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-( function () {
-       /**
-        * List displaying all filter groups
-        *
-        * @extends OO.ui.Widget
-        * @mixins OO.ui.mixin.PendingElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
-        * @param {Object} [config] Configuration object
-        * @cfg {Object} [filters] A definition of the filter groups in this list
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
-        *  system. If not given, falls back to this widget's $element
-        * @cfg {boolean} [collapsed] Filter area is collapsed
-        */
-       mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
-               controller, model, savedQueriesModel, changesListModel, config
-       ) {
-               var $bottom;
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.FilterWrapperWidget.parent.call( this, config );
-               // Mixin constructors
-               OO.ui.mixin.PendingElement.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-               this.queriesModel = savedQueriesModel;
-               this.changesListModel = changesListModel;
-               this.$overlay = config.$overlay || this.$element;
-               this.$wrapper = config.$wrapper || this.$element;
-
-               this.filterTagWidget = new mw.rcfilters.ui.FilterTagMultiselectWidget(
-                       this.controller,
-                       this.model,
-                       this.queriesModel,
-                       {
-                               $overlay: this.$overlay,
-                               collapsed: config.collapsed,
-                               $wrapper: this.$wrapper
-                       }
-               );
-
-               this.liveUpdateButton = new mw.rcfilters.ui.LiveUpdateButtonWidget(
-                       this.controller,
-                       this.changesListModel
-               );
-
-               this.numChangesAndDateWidget = new mw.rcfilters.ui.ChangesLimitAndDateButtonWidget(
-                       this.controller,
-                       this.model,
-                       {
-                               $overlay: this.$overlay
-                       }
-               );
-
-               this.showNewChangesLink = new OO.ui.ButtonWidget( {
-                       icon: 'reload',
-                       framed: false,
-                       label: mw.msg( 'rcfilters-show-new-changes' ),
-                       flags: [ 'progressive' ],
-                       classes: [ 'mw-rcfilters-ui-filterWrapperWidget-showNewChanges' ]
-               } );
-
-               // Events
-               this.filterTagWidget.menu.connect( this, { toggle: [ 'emit', 'menuToggle' ] } );
-               this.changesListModel.connect( this, { newChangesExist: 'onNewChangesExist' } );
-               this.showNewChangesLink.connect( this, { click: 'onShowNewChangesClick' } );
-               this.showNewChangesLink.toggle( false );
-
-               // Initialize
-               this.$top = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
-
-               $bottom = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
-                       .append(
-                               this.showNewChangesLink.$element,
-                               this.numChangesAndDateWidget.$element
-                       );
-
-               if ( mw.config.get( 'StructuredChangeFiltersLiveUpdatePollingRate' ) ) {
-                       $bottom.prepend( this.liveUpdateButton.$element );
-               }
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
-                       .append(
-                               this.$top,
-                               this.filterTagWidget.$element,
-                               $bottom
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FilterWrapperWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.FilterWrapperWidget, OO.ui.mixin.PendingElement );
-
-       /* Methods */
-
-       /**
-        * Set the content of the top section
-        *
-        * @param {jQuery} $topSectionElement
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.setTopSection = function ( $topSectionElement ) {
-               this.$top.append( $topSectionElement );
-       };
-
-       /**
-        * Respond to the user clicking the 'show new changes' button
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onShowNewChangesClick = function () {
-               this.controller.showNewChanges();
-       };
-
-       /**
-        * Respond to changes list model newChangesExist
-        *
-        * @param {boolean} newChangesExist Whether new changes exist
-        */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.onNewChangesExist = function ( newChangesExist ) {
-               this.showNewChangesLink.toggle( newChangesExist );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
deleted file mode 100644 (file)
index a28cde0..0000000
+++ /dev/null
@@ -1,173 +0,0 @@
-( function () {
-       /**
-        * Wrapper for the RC form with hide/show links
-        * Must be constructed after the model is initialized.
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Changes list view model
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changeListModel Changes list view model
-        * @param {mw.rcfilters.Controller} controller RCfilters controller
-        * @param {jQuery} $formRoot Root element of the form to attach to
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.FormWrapperWidget = function MwRcfiltersUiFormWrapperWidget( filtersModel, changeListModel, controller, $formRoot, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.FormWrapperWidget.parent.call( this, $.extend( {}, config, {
-                       $element: $formRoot
-               } ) );
-
-               this.changeListModel = changeListModel;
-               this.filtersModel = filtersModel;
-               this.controller = controller;
-               this.$submitButton = this.$element.find( 'form input[type=submit]' );
-
-               this.$element
-                       .on( 'click', 'a[data-params]', this.onLinkClick.bind( this ) );
-
-               this.$element
-                       .on( 'submit', 'form', this.onFormSubmit.bind( this ) );
-
-               // Events
-               this.changeListModel.connect( this, {
-                       invalidate: 'onChangesModelInvalidate',
-                       update: 'onChangesModelUpdate'
-               } );
-
-               // Initialize
-               this.cleanUpFieldset();
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-FormWrapperWidget' );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.FormWrapperWidget, OO.ui.Widget );
-
-       /**
-        * Respond to link click
-        *
-        * @param {jQuery.Event} e Event
-        * @return {boolean} false
-        */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.onLinkClick = function ( e ) {
-               this.controller.updateChangesList( $( e.target ).data( 'params' ) );
-               return false;
-       };
-
-       /**
-        * Respond to form submit event
-        *
-        * @param {jQuery.Event} e Event
-        * @return {boolean} false
-        */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.onFormSubmit = function ( e ) {
-               var data = {};
-
-               // Collect all data from form
-               $( e.target ).find( 'input:not([type="hidden"],[type="submit"]), select' ).each( function () {
-                       var value = '';
-
-                       if ( !$( this ).is( ':checkbox' ) || $( this ).is( ':checked' ) ) {
-                               value = $( this ).val();
-                       }
-
-                       data[ $( this ).prop( 'name' ) ] = value;
-               } );
-
-               this.controller.updateChangesList( data );
-               return false;
-       };
-
-       /**
-        * Respond to model invalidate
-        */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelInvalidate = function () {
-               this.$submitButton.prop( 'disabled', true );
-       };
-
-       /**
-        * Respond to model update, replace the show/hide links with the ones from the
-        * server so they feature the correct state.
-        *
-        * @param {jQuery|string} $changesList Updated changes list
-        * @param {jQuery} $fieldset Updated fieldset
-        * @param {string} noResultsDetails Type of no result error
-        * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
-        */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.onChangesModelUpdate = function ( $changesList, $fieldset, noResultsDetails, isInitialDOM ) {
-               this.$submitButton.prop( 'disabled', false );
-
-               // Replace the entire fieldset
-               this.$element.empty().append( $fieldset.contents() );
-
-               if ( !isInitialDOM ) {
-                       // Make sure enhanced RC re-initializes correctly
-                       mw.hook( 'wikipage.content' ).fire( this.$element );
-               }
-
-               this.cleanUpFieldset();
-       };
-
-       /**
-        * Clean up the old-style show/hide that we have implemented in the filter list
-        */
-       mw.rcfilters.ui.FormWrapperWidget.prototype.cleanUpFieldset = function () {
-               this.$element.find( '.clshowhideoption[data-feature-in-structured-ui=1]' ).each( function () {
-                       // HACK: Remove the text node after the span.
-                       // If there isn't one, we're at the end, so remove the text node before the span.
-                       // This would be unnecessary if we added separators with CSS.
-                       if ( this.nextSibling && this.nextSibling.nodeType === Node.TEXT_NODE ) {
-                               this.parentNode.removeChild( this.nextSibling );
-                       } else if ( this.previousSibling && this.previousSibling.nodeType === Node.TEXT_NODE ) {
-                               this.parentNode.removeChild( this.previousSibling );
-                       }
-                       // Remove the span itself
-                       this.parentNode.removeChild( this );
-               } );
-
-               // Hide namespaces and tags
-               this.$element.find( '.namespaceForm' ).detach();
-               this.$element.find( '.mw-tagfilter-label' ).closest( 'tr' ).detach();
-
-               // Hide Related Changes page name form
-               this.$element.find( '.targetForm' ).detach();
-
-               // misc: limit, days, watchlist info msg
-               this.$element.find( '.rclinks, .cldays, .wlinfo' ).detach();
-
-               if ( !this.$element.find( '.mw-recentchanges-table tr' ).length ) {
-                       this.$element.find( '.mw-recentchanges-table' ).detach();
-                       this.$element.find( 'hr' ).detach();
-               }
-
-               // Get rid of all <br>s, which are inside rcshowhide
-               // If we still have content in rcshowhide, the <br>s are
-               // gone. Instead, the CSS now has a rule to mark all <span>s
-               // inside .rcshowhide with display:block; to simulate newlines
-               // where they're actually needed.
-               this.$element.find( 'br' ).detach();
-               if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
-                       this.$element.find( '.rcshowhide' ).detach();
-               }
-
-               if ( this.$element.find( '.cloption' ).text().trim() === '' ) {
-                       this.$element.find( '.cloption-submit' ).detach();
-               }
-
-               this.$element.find(
-                       '.rclistfrom, .rcnotefrom, .rcoptions-listfromreset'
-               ).detach();
-
-               // Get rid of the legend
-               this.$element.find( 'legend' ).detach();
-
-               // Check if the element is essentially empty, and detach it if it is
-               if ( !this.$element.text().trim().length ) {
-                       this.$element.detach();
-               }
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.GroupWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.GroupWidget.js
deleted file mode 100644 (file)
index ab49414..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-( function () {
-       /**
-        * A group widget to allow for aggregation of events
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {Object} [config] Configuration object
-        * @param {Object} [events] Events to aggregate. The object represent the
-        *  event name to aggregate and the event value to emit on aggregate for items.
-        */
-       mw.rcfilters.ui.GroupWidget = function MwRcfiltersUiViewSwitchWidget( config ) {
-               var aggregate = {};
-
-               config = config || {};
-
-               // Parent constructor
-               mw.rcfilters.ui.GroupWidget.parent.call( this, config );
-
-               // Mixin constructors
-               OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
-
-               if ( config.events ) {
-                       // Aggregate events
-                       // eslint-disable-next-line jquery/no-each-util
-                       $.each( config.events, function ( eventName, eventEmit ) {
-                               aggregate[ eventName ] = eventEmit;
-                       } );
-
-                       this.aggregate( aggregate );
-               }
-
-               if ( Array.isArray( config.items ) ) {
-                       this.addItems( config.items );
-               }
-       };
-
-       /* Initialize */
-
-       OO.inheritClass( mw.rcfilters.ui.GroupWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.GroupWidget, OO.ui.mixin.GroupWidget );
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js
deleted file mode 100644 (file)
index a55246f..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-( function () {
-       /**
-        * A widget representing a filter item highlight color picker
-        *
-        * @extends OO.ui.Widget
-        * @mixins OO.ui.mixin.LabelElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.HighlightColorPickerWidget = function MwRcfiltersUiHighlightColorPickerWidget( controller, config ) {
-               var colors = [ 'none' ].concat( mw.rcfilters.HighlightColors );
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.HighlightColorPickerWidget.parent.call( this, config );
-               // Mixin constructors
-               OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
-                       label: mw.message( 'rcfilters-highlightmenu-title' ).text()
-               } ) );
-
-               this.controller = controller;
-
-               this.currentSelection = 'none';
-               this.buttonSelect = new OO.ui.ButtonSelectWidget( {
-                       items: colors.map( function ( color ) {
-                               return new OO.ui.ButtonOptionWidget( {
-                                       icon: color === 'none' ? 'check' : null,
-                                       data: color,
-                                       classes: [
-                                               'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color',
-                                               'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color-' + color
-                                       ],
-                                       framed: false
-                               } );
-                       } ),
-                       classes: 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect'
-               } );
-
-               // Event
-               this.buttonSelect.connect( this, { choose: 'onChooseColor' } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget' )
-                       .append(
-                               this.$label
-                                       .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget-label' ),
-                               this.buttonSelect.$element
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.HighlightColorPickerWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.HighlightColorPickerWidget, OO.ui.mixin.LabelElement );
-
-       /* Events */
-
-       /**
-        * @event chooseColor
-        * @param {string} The chosen color
-        *
-        * A color has been chosen
-        */
-
-       /* Methods */
-
-       /**
-        * Bind the color picker to an item
-        * @param {mw.rcfilters.dm.FilterItem} filterItem
-        */
-       mw.rcfilters.ui.HighlightColorPickerWidget.prototype.setFilterItem = function ( filterItem ) {
-               if ( this.filterItem ) {
-                       this.filterItem.disconnect( this );
-               }
-
-               this.filterItem = filterItem;
-               this.filterItem.connect( this, { update: 'updateUiBasedOnModel' } );
-               this.updateUiBasedOnModel();
-       };
-
-       /**
-        * Respond to item model update event
-        */
-       mw.rcfilters.ui.HighlightColorPickerWidget.prototype.updateUiBasedOnModel = function () {
-               this.selectColor( this.filterItem.getHighlightColor() || 'none' );
-       };
-
-       /**
-        * Select the color for this widget
-        *
-        * @param {string} color Selected color
-        */
-       mw.rcfilters.ui.HighlightColorPickerWidget.prototype.selectColor = function ( color ) {
-               var previousItem = this.buttonSelect.findItemFromData( this.currentSelection ),
-                       selectedItem = this.buttonSelect.findItemFromData( color );
-
-               if ( this.currentSelection !== color ) {
-                       this.currentSelection = color;
-
-                       this.buttonSelect.selectItem( selectedItem );
-                       if ( previousItem ) {
-                               previousItem.setIcon( null );
-                       }
-
-                       if ( selectedItem ) {
-                               selectedItem.setIcon( 'check' );
-                       }
-               }
-       };
-
-       mw.rcfilters.ui.HighlightColorPickerWidget.prototype.onChooseColor = function ( button ) {
-               var color = button.data;
-               if ( color === 'none' ) {
-                       this.controller.clearHighlightColor( this.filterItem.getName() );
-               } else {
-                       this.controller.setHighlightColor( this.filterItem.getName(), color );
-               }
-               this.emit( 'chooseColor', color );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightPopupWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightPopupWidget.js
deleted file mode 100644 (file)
index 2dd0379..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-( function () {
-       /**
-        * A popup containing a color picker, for setting highlight colors.
-        *
-        * @extends OO.ui.PopupWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.HighlightPopupWidget = function MwRcfiltersUiHighlightPopupWidget( controller, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.HighlightPopupWidget.parent.call( this, $.extend( {
-                       autoClose: true,
-                       anchor: false,
-                       padded: true,
-                       align: 'backwards',
-                       horizontalPosition: 'end',
-                       width: 290
-               }, config ) );
-
-               this.colorPicker = new mw.rcfilters.ui.HighlightColorPickerWidget( controller );
-
-               this.colorPicker.connect( this, { chooseColor: 'onChooseColor' } );
-
-               this.$body.append( this.colorPicker.$element );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.HighlightPopupWidget, OO.ui.PopupWidget );
-
-       /* Methods */
-
-       /**
-        * Set the button (or other widget) that this popup should hang off.
-        *
-        * @param {OO.ui.Widget} widget Widget the popup should orient itself to
-        */
-       mw.rcfilters.ui.HighlightPopupWidget.prototype.setAssociatedButton = function ( widget ) {
-               this.setFloatableContainer( widget.$element );
-               this.$autoCloseIgnore = widget.$element;
-       };
-
-       /**
-        * Set the filter item that this popup should control the highlight color for.
-        *
-        * @param {mw.rcfilters.dm.FilterItem} item
-        */
-       mw.rcfilters.ui.HighlightPopupWidget.prototype.setFilterItem = function ( item ) {
-               this.colorPicker.setFilterItem( item );
-       };
-
-       /**
-        * When the user chooses a color in the color picker, close the popup.
-        */
-       mw.rcfilters.ui.HighlightPopupWidget.prototype.onChooseColor = function () {
-               this.toggle( false );
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js
deleted file mode 100644 (file)
index cda13eb..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-( function () {
-       /**
-        * A widget representing a base toggle item
-        *
-        * @extends OO.ui.MenuOptionWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller RCFilters controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
-        * @param {mw.rcfilters.dm.ItemModel} invertModel
-        * @param {mw.rcfilters.dm.ItemModel} itemModel Item model
-        * @param {mw.rcfilters.ui.HighlightPopupWidget} highlightPopup Shared highlight color picker
-        * @param {Object} config Configuration object
-        */
-       mw.rcfilters.ui.ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget(
-               controller, filtersViewModel, invertModel, itemModel, highlightPopup, config
-       ) {
-               var layout,
-                       classes = [],
-                       $label = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label' );
-
-               config = config || {};
-
-               this.controller = controller;
-               this.filtersViewModel = filtersViewModel;
-               this.invertModel = invertModel;
-               this.itemModel = itemModel;
-
-               // Parent
-               mw.rcfilters.ui.ItemMenuOptionWidget.parent.call( this, $.extend( {
-                       // Override the 'check' icon that OOUI defines
-                       icon: '',
-                       data: this.itemModel.getName(),
-                       label: this.itemModel.getLabel()
-               }, config ) );
-
-               this.checkboxWidget = new mw.rcfilters.ui.CheckboxInputWidget( {
-                       value: this.itemModel.getName(),
-                       selected: this.itemModel.isSelected()
-               } );
-
-               $label.append(
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-title' )
-                               .append( $( '<bdi>' ).append( this.$label ) )
-               );
-               if ( this.itemModel.getDescription() ) {
-                       $label.append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-label-desc' )
-                                       .append( $( '<bdi>' ).text( this.itemModel.getDescription() ) )
-                       );
-               }
-
-               this.highlightButton = new mw.rcfilters.ui.FilterItemHighlightButton(
-                       this.controller,
-                       this.itemModel,
-                       highlightPopup,
-                       {
-                               $overlay: config.$overlay || this.$element,
-                               title: mw.msg( 'rcfilters-highlightmenu-help' )
-                       }
-               );
-               this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
-
-               this.excludeLabel = new OO.ui.LabelWidget( {
-                       label: mw.msg( 'rcfilters-filter-excluded' )
-               } );
-               this.excludeLabel.toggle(
-                       this.itemModel.getGroupModel().getView() === 'namespaces' &&
-                       this.itemModel.isSelected() &&
-                       this.invertModel.isSelected()
-               );
-
-               layout = new OO.ui.FieldLayout( this.checkboxWidget, {
-                       label: $label,
-                       align: 'inline'
-               } );
-
-               // Events
-               this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
-               this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
-               this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
-               // HACK: Prevent defaults on 'click' for the label so it
-               // doesn't steal the focus away from the input. This means
-               // we can continue arrow-movement after we click the label
-               // and is consistent with the checkbox *itself* also preventing
-               // defaults on 'click' as well.
-               layout.$label.on( 'click', false );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget' )
-                       .addClass( 'mw-rcfilters-ui-itemMenuOptionWidget-view-' + this.itemModel.getGroupModel().getView() )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-itemCheckbox' )
-                                                                       .append( layout.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-excludeLabel' )
-                                                                       .append( this.excludeLabel.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-itemMenuOptionWidget-highlightButton' )
-                                                                       .append( this.highlightButton.$element )
-                                                       )
-                                       )
-                       );
-
-               if ( this.itemModel.getIdentifiers() ) {
-                       this.itemModel.getIdentifiers().forEach( function ( ident ) {
-                               classes.push( 'mw-rcfilters-ui-itemMenuOptionWidget-identifier-' + ident );
-                       } );
-
-                       this.$element.addClass( classes );
-               }
-
-               this.updateUiBasedOnState();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.ItemMenuOptionWidget, OO.ui.MenuOptionWidget );
-
-       /* Static properties */
-
-       // We do our own scrolling to top
-       mw.rcfilters.ui.ItemMenuOptionWidget.static.scrollIntoViewOnSelect = false;
-
-       /* Methods */
-
-       /**
-        * Respond to item model update event
-        */
-       mw.rcfilters.ui.ItemMenuOptionWidget.prototype.updateUiBasedOnState = function () {
-               this.checkboxWidget.setSelected( this.itemModel.isSelected() );
-
-               this.highlightButton.toggle( this.filtersViewModel.isHighlightEnabled() );
-               this.excludeLabel.toggle(
-                       this.itemModel.getGroupModel().getView() === 'namespaces' &&
-                       this.itemModel.isSelected() &&
-                       this.invertModel.isSelected()
-               );
-               this.toggle( this.itemModel.isVisible() );
-       };
-
-       /**
-        * Get the name of this filter
-        *
-        * @return {string} Filter name
-        */
-       mw.rcfilters.ui.ItemMenuOptionWidget.prototype.getName = function () {
-               return this.itemModel.getName();
-       };
-
-       mw.rcfilters.ui.ItemMenuOptionWidget.prototype.getModel = function () {
-               return this.itemModel;
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js
deleted file mode 100644 (file)
index 926ff4a..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-( function () {
-       /**
-        * Widget for toggling live updates
-        *
-        * @extends OO.ui.ToggleButtonWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, changesListModel, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.LiveUpdateButtonWidget.parent.call( this, $.extend( {
-                       label: mw.message( 'rcfilters-liveupdates-button' ).text()
-               }, config ) );
-
-               this.controller = controller;
-               this.model = changesListModel;
-
-               // Events
-               this.connect( this, { click: 'onClick' } );
-               this.model.connect( this, { liveUpdateChange: 'onLiveUpdateChange' } );
-
-               this.$element.addClass( 'mw-rcfilters-ui-liveUpdateButtonWidget' );
-
-               this.setState( false );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.LiveUpdateButtonWidget, OO.ui.ToggleButtonWidget );
-
-       /* Methods */
-
-       /**
-        * Respond to the button being clicked
-        */
-       mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.onClick = function () {
-               this.controller.toggleLiveUpdate();
-       };
-
-       /**
-        * Set the button's state and change its appearance
-        *
-        * @param {boolean} enable Whether the 'live update' feature is now on/off
-        */
-       mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.setState = function ( enable ) {
-               this.setValue( enable );
-               this.setIcon( enable ? 'stop' : 'play' );
-               this.setTitle( mw.message(
-                       enable ?
-                               'rcfilters-liveupdates-button-title-on' :
-                               'rcfilters-liveupdates-button-title-off'
-               ).text() );
-       };
-
-       /**
-        * Respond to the 'live update' feature being turned on/off
-        *
-        * @param {boolean} enable Whether the 'live update' feature is now on/off
-        */
-       mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.onLiveUpdateChange = function ( enable ) {
-               this.setState( enable );
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MainWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MainWrapperWidget.js
deleted file mode 100644 (file)
index b402627..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-( function () {
-       /**
-        * Wrapper for changes list content
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} $topSection Top section container
-        * @cfg {jQuery} $filtersContainer
-        * @cfg {jQuery} $changesListContainer
-        * @cfg {jQuery} $formContainer
-        * @cfg {boolean} [collapsed] Filter area is collapsed
-        * @cfg {jQuery} [$wrapper] A jQuery object for the wrapper of the general
-        *  system. If not given, falls back to this widget's $element
-        */
-       mw.rcfilters.ui.MainWrapperWidget = function MwRcfiltersUiMainWrapperWidget(
-               controller, model, savedQueriesModel, changesListModel, config
-       ) {
-               config = $.extend( {}, config );
-
-               // Parent
-               mw.rcfilters.ui.MainWrapperWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-               this.changesListModel = changesListModel;
-               this.$topSection = config.$topSection;
-               this.$filtersContainer = config.$filtersContainer;
-               this.$changesListContainer = config.$changesListContainer;
-               this.$formContainer = config.$formContainer;
-               this.$overlay = $( '<div>' ).addClass( 'mw-rcfilters-ui-overlay' );
-               this.$wrapper = config.$wrapper || this.$element;
-
-               this.savedLinksListWidget = new mw.rcfilters.ui.SavedLinksListWidget(
-                       controller, savedQueriesModel, { $overlay: this.$overlay }
-               );
-
-               this.filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
-                       controller,
-                       model,
-                       savedQueriesModel,
-                       changesListModel,
-                       {
-                               $overlay: this.$overlay,
-                               $wrapper: this.$wrapper,
-                               collapsed: config.collapsed
-                       }
-               );
-
-               this.changesListWidget = new mw.rcfilters.ui.ChangesListWrapperWidget(
-                       model, changesListModel, controller, this.$changesListContainer );
-
-               /* Events */
-
-               // Toggle changes list overlay when filters menu opens/closes. We use overlay on changes list
-               // to prevent users from accidentally clicking on links in results, while menu is opened.
-               // Overlay on changes list is not the same as this.$overlay
-               this.filtersWidget.connect( this, { menuToggle: this.onFilterMenuToggle.bind( this ) } );
-
-               // Initialize
-               this.$filtersContainer.append( this.filtersWidget.$element );
-               $( 'body' )
-                       .append( this.$overlay )
-                       .addClass( 'mw-rcfilters-ui-initialized' );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.MainWrapperWidget, OO.ui.Widget );
-
-       /* Methods */
-
-       /**
-        * Set the content of the top section, depending on the type of special page.
-        *
-        * @param {string} specialPage
-        */
-       mw.rcfilters.ui.MainWrapperWidget.prototype.setTopSection = function ( specialPage ) {
-               var topSection;
-
-               if ( specialPage === 'Recentchanges' ) {
-                       topSection = new mw.rcfilters.ui.RcTopSectionWidget(
-                               this.savedLinksListWidget, this.$topSection
-                       );
-                       this.filtersWidget.setTopSection( topSection.$element );
-               }
-
-               if ( specialPage === 'Recentchangeslinked' ) {
-                       topSection = new mw.rcfilters.ui.RclTopSectionWidget(
-                               this.savedLinksListWidget, this.controller,
-                               this.model.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' ),
-                               this.model.getGroup( 'page' ).getItemByParamName( 'target' )
-                       );
-
-                       this.filtersWidget.setTopSection( topSection.$element );
-               }
-
-               if ( specialPage === 'Watchlist' ) {
-                       topSection = new mw.rcfilters.ui.WatchlistTopSectionWidget(
-                               this.controller, this.changesListModel, this.savedLinksListWidget, this.$topSection
-                       );
-
-                       this.filtersWidget.setTopSection( topSection.$element );
-               }
-       };
-
-       /**
-        * Filter menu toggle event listener
-        *
-        * @param {boolean} isVisible
-        */
-       mw.rcfilters.ui.MainWrapperWidget.prototype.onFilterMenuToggle = function ( isVisible ) {
-               this.changesListWidget.toggleOverlay( isVisible );
-       };
-
-       /**
-        * Initialize FormWrapperWidget
-        *
-        * @return {mw.rcfilters.ui.FormWrapperWidget} Form wrapper widget
-        */
-       mw.rcfilters.ui.MainWrapperWidget.prototype.initFormWidget = function () {
-               return new mw.rcfilters.ui.FormWrapperWidget(
-                       this.model, this.changesListModel, this.controller, this.$formContainer );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js
deleted file mode 100644 (file)
index 328be8c..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-( function () {
-       /**
-        * Button for marking all changes as seen on the Watchlist
-        *
-        * @extends OO.ui.ButtonWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.ChangesListViewModel} model Changes list view model
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.MarkSeenButtonWidget = function MwRcfiltersUiMarkSeenButtonWidget( controller, model, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.MarkSeenButtonWidget.parent.call( this, $.extend( {
-                       label: mw.message( 'rcfilters-watchlist-markseen-button' ).text(),
-                       icon: 'checkAll'
-               }, config ) );
-
-               this.controller = controller;
-               this.model = model;
-
-               // Events
-               this.connect( this, { click: 'onClick' } );
-               this.model.connect( this, { update: 'onModelUpdate' } );
-
-               this.$element.addClass( 'mw-rcfilters-ui-markSeenButtonWidget' );
-
-               this.onModelUpdate();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.MarkSeenButtonWidget, OO.ui.ButtonWidget );
-
-       /* Methods */
-
-       /**
-        * Respond to the button being clicked
-        */
-       mw.rcfilters.ui.MarkSeenButtonWidget.prototype.onClick = function () {
-               this.controller.markAllChangesAsSeen();
-               // assume there's no more unseen changes until the next model update
-               this.setDisabled( true );
-       };
-
-       /**
-        * Respond to the model being updated with new changes
-        */
-       mw.rcfilters.ui.MarkSeenButtonWidget.prototype.onModelUpdate = function () {
-               this.setDisabled( !this.model.hasUnseenWatchedChanges() );
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js
deleted file mode 100644 (file)
index 49f980c..0000000
+++ /dev/null
@@ -1,359 +0,0 @@
-( function () {
-       /**
-        * A floating menu widget for the filter list
-        *
-        * @extends OO.ui.MenuSelectWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {Object} [config] Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        * @cfg {Object[]} [footers] An array of objects defining the footers for
-        *  this menu, with a definition whether they appear per specific views.
-        *  The expected structure is:
-        *  [
-        *     {
-        *        name: {string} A unique name for the footer object
-        *        $element: {jQuery} A jQuery object for the content of the footer
-        *        views: {string[]} Optional. An array stating which views this footer is
-        *               active on. Use null or omit to display this on all views.
-        *     }
-        *  ]
-        */
-       mw.rcfilters.ui.MenuSelectWidget = function MwRcfiltersUiMenuSelectWidget( controller, model, config ) {
-               var header;
-
-               config = config || {};
-
-               this.controller = controller;
-               this.model = model;
-               this.currentView = '';
-               this.views = {};
-               this.userSelecting = false;
-
-               this.menuInitialized = false;
-               this.$overlay = config.$overlay || this.$element;
-               this.$body = $( '<div>' ).addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
-               this.footers = [];
-
-               // Parent
-               mw.rcfilters.ui.MenuSelectWidget.parent.call( this, $.extend( config, {
-                       $autoCloseIgnore: this.$overlay,
-                       width: 650,
-                       // Our filtering is done through the model
-                       filterFromInput: false
-               } ) );
-               this.setGroupElement(
-                       $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
-               );
-               this.setClippableElement( this.$body );
-               this.setClippableContainer( this.$element );
-
-               header = new mw.rcfilters.ui.FilterMenuHeaderWidget(
-                       this.controller,
-                       this.model,
-                       {
-                               $overlay: this.$overlay
-                       }
-               );
-
-               this.noResults = new OO.ui.LabelWidget( {
-                       label: mw.msg( 'rcfilters-filterlist-noresults' ),
-                       classes: [ 'mw-rcfilters-ui-menuSelectWidget-noresults' ]
-               } );
-
-               // Events
-               this.model.connect( this, {
-                       initialize: 'onModelInitialize',
-                       searchChange: 'onModelSearchChange'
-               } );
-
-               // Initialization
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-menuSelectWidget' )
-                       .append( header.$element )
-                       .append(
-                               this.$body
-                                       .append( this.$group, this.noResults.$element )
-                       );
-
-               // Append all footers; we will control their visibility
-               // based on view
-               config.footers = config.footers || [];
-               config.footers.forEach( function ( footerData ) {
-                       var isSticky = footerData.sticky === undefined ? true : !!footerData.sticky,
-                               adjustedData = {
-                                       // Wrap the element with our own footer wrapper
-                                       $element: $( '<div>' )
-                                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer' )
-                                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer-' + footerData.name )
-                                               .append( footerData.$element ),
-                                       views: footerData.views
-                               };
-
-                       if ( !footerData.disabled ) {
-                               this.footers.push( adjustedData );
-
-                               if ( isSticky ) {
-                                       this.$element.append( adjustedData.$element );
-                               } else {
-                                       this.$body.append( adjustedData.$element );
-                               }
-                       }
-               }.bind( this ) );
-
-               // Switch to the correct view
-               this.updateView();
-       };
-
-       /* Initialize */
-
-       OO.inheritClass( mw.rcfilters.ui.MenuSelectWidget, OO.ui.MenuSelectWidget );
-
-       /* Events */
-
-       /* Methods */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.onModelSearchChange = function () {
-               this.updateView();
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.toggle = function ( show ) {
-               this.lazyMenuCreation();
-               mw.rcfilters.ui.MenuSelectWidget.parent.prototype.toggle.call( this, show );
-               // Always open this menu downwards. FilterTagMultiselectWidget scrolls it into view.
-               this.setVerticalPosition( 'below' );
-       };
-
-       /**
-        * lazy creation of the menu
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.lazyMenuCreation = function () {
-               var widget = this,
-                       items = [],
-                       viewGroupCount = {},
-                       groups = this.model.getFilterGroups();
-
-               if ( this.menuInitialized ) {
-                       return;
-               }
-
-               this.menuInitialized = true;
-
-               // Create shared popup for highlight buttons
-               this.highlightPopup = new mw.rcfilters.ui.HighlightPopupWidget( this.controller );
-               this.$overlay.append( this.highlightPopup.$element );
-
-               // Count groups per view
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( groups, function ( groupName, groupModel ) {
-                       if ( !groupModel.isHidden() ) {
-                               viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
-                               viewGroupCount[ groupModel.getView() ]++;
-                       }
-               } );
-
-               // eslint-disable-next-line jquery/no-each-util
-               $.each( groups, function ( groupName, groupModel ) {
-                       var currentItems = [],
-                               view = groupModel.getView();
-
-                       if ( !groupModel.isHidden() ) {
-                               if ( viewGroupCount[ view ] > 1 ) {
-                                       // Only add a section header if there is more than
-                                       // one group
-                                       currentItems.push(
-                                               // Group section
-                                               new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
-                                                       widget.controller,
-                                                       groupModel,
-                                                       {
-                                                               $overlay: widget.$overlay
-                                                       }
-                                               )
-                                       );
-                               }
-
-                               // Add items
-                               widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
-                                       currentItems.push(
-                                               new mw.rcfilters.ui.FilterMenuOptionWidget(
-                                                       widget.controller,
-                                                       widget.model,
-                                                       widget.model.getInvertModel(),
-                                                       filterItem,
-                                                       widget.highlightPopup,
-                                                       {
-                                                               $overlay: widget.$overlay
-                                                       }
-                                               )
-                                       );
-                               } );
-
-                               // Cache the items per view, so we can switch between them
-                               // without rebuilding the widgets each time
-                               widget.views[ view ] = widget.views[ view ] || [];
-                               widget.views[ view ] = widget.views[ view ].concat( currentItems );
-                               items = items.concat( currentItems );
-                       }
-               } );
-
-               this.addItems( items );
-               this.updateView();
-       };
-
-       /**
-        * Respond to model initialize event. Populate the menu from the model
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.onModelInitialize = function () {
-               this.menuInitialized = false;
-               // Set timeout for the menu to lazy build.
-               setTimeout( this.lazyMenuCreation.bind( this ) );
-       };
-
-       /**
-        * Update view
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.updateView = function () {
-               var viewName = this.model.getCurrentView();
-
-               if ( this.views[ viewName ] && this.currentView !== viewName ) {
-                       this.updateFooterVisibility( viewName );
-
-                       this.$element
-                               .data( 'view', viewName )
-                               .removeClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + this.currentView )
-                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + viewName );
-
-                       this.currentView = viewName;
-                       this.scrollToTop();
-               }
-
-               this.postProcessItems();
-               this.clip();
-       };
-
-       /**
-        * Go over the available footers and decide which should be visible
-        * for this view
-        *
-        * @param {string} [currentView] Current view
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.updateFooterVisibility = function ( currentView ) {
-               currentView = currentView || this.model.getCurrentView();
-
-               this.footers.forEach( function ( data ) {
-                       data.$element.toggle(
-                               // This footer should only be shown if it is configured
-                               // for all views or for this specific view
-                               !data.views || data.views.length === 0 || data.views.indexOf( currentView ) > -1
-                       );
-               } );
-       };
-
-       /**
-        * Post-process items after the visibility changed. Make sure
-        * that we always have an item selected, and that the no-results
-        * widget appears if the menu is empty.
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.postProcessItems = function () {
-               var i,
-                       itemWasSelected = false,
-                       items = this.getItems();
-
-               // If we are not already selecting an item, always make sure
-               // that the top item is selected
-               if ( !this.userSelecting ) {
-                       // Select the first item in the list
-                       for ( i = 0; i < items.length; i++ ) {
-                               if (
-                                       !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
-                                       items[ i ].isVisible()
-                               ) {
-                                       itemWasSelected = true;
-                                       this.selectItem( items[ i ] );
-                                       break;
-                               }
-                       }
-
-                       if ( !itemWasSelected ) {
-                               this.selectItem( null );
-                       }
-               }
-
-               this.noResults.toggle( !this.getItems().some( function ( item ) {
-                       return item.isVisible();
-               } ) );
-       };
-
-       /**
-        * Get the option widget that matches the model given
-        *
-        * @param {mw.rcfilters.dm.ItemModel} model Item model
-        * @return {mw.rcfilters.ui.ItemMenuOptionWidget} Option widget
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.getItemFromModel = function ( model ) {
-               this.lazyMenuCreation();
-               return this.views[ model.getGroupModel().getView() ].filter( function ( item ) {
-                       return item.getName() === model.getName();
-               } )[ 0 ];
-       };
-
-       /**
-        * @inheritdoc
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
-               var nextItem,
-                       currentItem = this.findHighlightedItem() || this.findSelectedItem();
-
-               // Call parent
-               mw.rcfilters.ui.MenuSelectWidget.parent.prototype.onDocumentKeyDown.call( this, e );
-
-               // We want to select the item on arrow movement
-               // rather than just highlight it, like the menu
-               // does by default
-               if ( !this.isDisabled() && this.isVisible() ) {
-                       switch ( e.keyCode ) {
-                               case OO.ui.Keys.UP:
-                               case OO.ui.Keys.LEFT:
-                                       // Get the next item
-                                       nextItem = this.findRelativeSelectableItem( currentItem, -1 );
-                                       break;
-                               case OO.ui.Keys.DOWN:
-                               case OO.ui.Keys.RIGHT:
-                                       // Get the next item
-                                       nextItem = this.findRelativeSelectableItem( currentItem, 1 );
-                                       break;
-                       }
-
-                       nextItem = nextItem && nextItem.constructor.static.selectable ?
-                               nextItem : null;
-
-                       // Select the next item
-                       this.selectItem( nextItem );
-               }
-       };
-
-       /**
-        * Scroll to the top of the menu
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.scrollToTop = function () {
-               this.$body.scrollTop( 0 );
-       };
-
-       /**
-        * Set whether the user is currently selecting an item.
-        * This is important when the user selects an item that is in between
-        * different views, and makes sure we do not re-select a different
-        * item (like the item on top) when this is happening.
-        *
-        * @param {boolean} isSelecting User is selecting
-        */
-       mw.rcfilters.ui.MenuSelectWidget.prototype.setUserSelecting = function ( isSelecting ) {
-               this.userSelecting = !!isSelecting;
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js
deleted file mode 100644 (file)
index e3d5575..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-( function () {
-       /**
-        * Top section (between page title and filters) on Special:Recentchanges
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
-        * @param {jQuery} $topLinks Content of the community-defined links
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.RcTopSectionWidget = function MwRcfiltersUiRcTopSectionWidget(
-               savedLinksListWidget, $topLinks, config
-       ) {
-               var toplinksTitle,
-                       topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
-                       topLinksCookie = mw.cookie.get( topLinksCookieName ),
-                       topLinksCookieValue = topLinksCookie || 'collapsed',
-                       widget = this;
-
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.RcTopSectionWidget.parent.call( this, config );
-
-               this.$topLinks = $topLinks;
-
-               toplinksTitle = new OO.ui.ButtonWidget( {
-                       framed: false,
-                       indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
-                       flags: [ 'progressive' ],
-                       label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
-               } );
-
-               this.$topLinks
-                       .makeCollapsible( {
-                               collapsed: topLinksCookieValue === 'collapsed',
-                               $customTogglers: toplinksTitle.$element
-                       } )
-                       .on( 'beforeExpand.mw-collapsible', function () {
-                               mw.cookie.set( topLinksCookieName, 'expanded' );
-                               toplinksTitle.setIndicator( 'up' );
-                               widget.switchTopLinks( 'expanded' );
-                       } )
-                       .on( 'beforeCollapse.mw-collapsible', function () {
-                               mw.cookie.set( topLinksCookieName, 'collapsed' );
-                               toplinksTitle.setIndicator( 'down' );
-                               widget.switchTopLinks( 'collapsed' );
-                       } );
-
-               this.$topLinks.find( '.mw-recentchanges-toplinks-title' )
-                       .replaceWith( toplinksTitle.$element.removeAttr( 'tabIndex' ) );
-
-               // Create two positions for the toplinks to toggle between
-               // in the table (first cell) or up above it
-               this.$top = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-top' );
-               this.$tableTopLinks = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-cell' )
-                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-table' );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               this.$tableTopLinks,
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-table-placeholder' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' ),
-                                                               !mw.user.isAnon() ?
-                                                                       $( '<div>' )
-                                                                               .addClass( 'mw-rcfilters-ui-cell' )
-                                                                               .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-savedLinks' )
-                                                                               .append( savedLinksListWidget.$element ) :
-                                                                       null
-                                                       )
-                                       )
-                       );
-
-               // Hack: For jumpiness reasons, this should be a sibling of -head
-               $( '.rcfilters-head' ).before( this.$top );
-
-               // Initialize top links position
-               widget.switchTopLinks( topLinksCookieValue );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.RcTopSectionWidget, OO.ui.Widget );
-
-       /**
-        * Switch the top links widget from inside the table (when collapsed)
-        * to the 'top' (when open)
-        *
-        * @param {string} [state] The state of the top links widget: 'expanded' or 'collapsed'
-        */
-       mw.rcfilters.ui.RcTopSectionWidget.prototype.switchTopLinks = function ( state ) {
-               state = state || 'expanded';
-
-               if ( state === 'expanded' ) {
-                       this.$top.append( this.$topLinks );
-               } else {
-                       this.$tableTopLinks.append( this.$topLinks );
-               }
-               this.$topLinks.toggleClass( 'mw-recentchanges-toplinks-collapsed', state === 'collapsed' );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js
deleted file mode 100644 (file)
index dc76085..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-( function () {
-       /**
-        * Widget to select and display target page on Special:RecentChangesLinked (AKA Related Changes)
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.FilterItem} targetPageModel
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.RclTargetPageWidget = function MwRcfiltersUiRclTargetPageWidget(
-               controller, targetPageModel, config
-       ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.RclTargetPageWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = targetPageModel;
-
-               this.titleSearch = new mw.widgets.TitleInputWidget( {
-                       validate: false,
-                       placeholder: mw.msg( 'rcfilters-target-page-placeholder' ),
-                       showImages: true,
-                       showDescriptions: true,
-                       addQueryInput: false
-               } );
-
-               // Events
-               this.model.connect( this, { update: 'updateUiBasedOnModel' } );
-
-               this.titleSearch.$input.on( {
-                       blur: this.onLookupInputBlur.bind( this )
-               } );
-
-               this.titleSearch.lookupMenu.connect( this, {
-                       choose: 'onLookupMenuItemChoose'
-               } );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-rclTargetPageWidget' )
-                       .append( this.titleSearch.$element );
-
-               this.updateUiBasedOnModel();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.RclTargetPageWidget, OO.ui.Widget );
-
-       /* Methods */
-
-       /**
-        * Respond to the user choosing a title
-        */
-       mw.rcfilters.ui.RclTargetPageWidget.prototype.onLookupMenuItemChoose = function () {
-               this.titleSearch.$input.trigger( 'blur' );
-       };
-
-       /**
-        * Respond to titleSearch $input blur
-        */
-       mw.rcfilters.ui.RclTargetPageWidget.prototype.onLookupInputBlur = function () {
-               this.controller.setTargetPage( this.titleSearch.getQueryValue() );
-       };
-
-       /**
-        * Respond to the model being updated
-        */
-       mw.rcfilters.ui.RclTargetPageWidget.prototype.updateUiBasedOnModel = function () {
-               var title = mw.Title.newFromText( this.model.getValue() ),
-                       text = title ? title.toText() : this.model.getValue();
-               this.titleSearch.setValue( text );
-               this.titleSearch.setTitle( text );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js
deleted file mode 100644 (file)
index 8925dcf..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-( function () {
-       /**
-        * Widget to select to view changes that link TO or FROM the target page
-        * on Special:RecentChangesLinked (AKA Related Changes)
-        *
-        * @extends OO.ui.DropdownWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel model this widget is bound to
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.RclToOrFromWidget = function MwRcfiltersUiRclToOrFromWidget(
-               controller, showLinkedToModel, config
-       ) {
-               config = config || {};
-
-               this.showLinkedFrom = new OO.ui.MenuOptionWidget( {
-                       data: 'from', // default (showlinkedto=0)
-                       label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedfrom-option-label' ) )
-               } );
-               this.showLinkedTo = new OO.ui.MenuOptionWidget( {
-                       data: 'to', // showlinkedto=1
-                       label: new OO.ui.HtmlSnippet( mw.msg( 'rcfilters-filter-showlinkedto-option-label' ) )
-               } );
-
-               // Parent
-               mw.rcfilters.ui.RclToOrFromWidget.parent.call( this, $.extend( {
-                       classes: [ 'mw-rcfilters-ui-rclToOrFromWidget' ],
-                       menu: { items: [ this.showLinkedFrom, this.showLinkedTo ] }
-               }, config ) );
-
-               this.controller = controller;
-               this.model = showLinkedToModel;
-
-               this.getMenu().connect( this, { choose: 'onUserChooseItem' } );
-               this.model.connect( this, { update: 'onModelUpdate' } );
-
-               // force an initial update of the component based on the state
-               this.onModelUpdate();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.RclToOrFromWidget, OO.ui.DropdownWidget );
-
-       /* Methods */
-
-       /**
-        * Respond to the user choosing an item in the menu
-        *
-        * @param {OO.ui.MenuOptionWidget} chosenItem
-        */
-       mw.rcfilters.ui.RclToOrFromWidget.prototype.onUserChooseItem = function ( chosenItem ) {
-               this.controller.setShowLinkedTo( chosenItem.getData() === 'to' );
-       };
-
-       /**
-        * Respond to model update
-        */
-       mw.rcfilters.ui.RclToOrFromWidget.prototype.onModelUpdate = function () {
-               this.getMenu().selectItem(
-                       this.model.isSelected() ?
-                               this.showLinkedTo :
-                               this.showLinkedFrom
-               );
-               this.setLabel( mw.msg(
-                       this.model.isSelected() ?
-                               'rcfilters-filter-showlinkedto-label' :
-                               'rcfilters-filter-showlinkedfrom-label'
-               ) );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js
deleted file mode 100644 (file)
index 7488254..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-( function () {
-       /**
-        * Top section (between page title and filters) on Special:RecentChangesLinked (AKA RelatedChanges)
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel Model for 'showlinkedto' parameter
-        * @param {mw.rcfilters.dm.FilterItem} targetPageModel Model for 'target' parameter
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.RclTopSectionWidget = function MwRcfiltersUiRclTopSectionWidget(
-               savedLinksListWidget, controller, showLinkedToModel, targetPageModel, config
-       ) {
-               var toOrFromWidget,
-                       targetPage;
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.RclTopSectionWidget.parent.call( this, config );
-
-               this.controller = controller;
-
-               toOrFromWidget = new mw.rcfilters.ui.RclToOrFromWidget( controller, showLinkedToModel );
-               targetPage = new mw.rcfilters.ui.RclTargetPageWidget( controller, targetPageModel );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-rclTopSectionWidget' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .append( toOrFromWidget.$element )
-                                                       ),
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .append( targetPage.$element ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-table-placeholder' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' ),
-                                                               !mw.user.isAnon() ?
-                                                                       $( '<div>' )
-                                                                               .addClass( 'mw-rcfilters-ui-cell' )
-                                                                               .addClass( 'mw-rcfilters-ui-rclTopSectionWidget-savedLinks' )
-                                                                               .append( savedLinksListWidget.$element ) :
-                                                                       null
-                                                       )
-                                       )
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.RclTopSectionWidget, OO.ui.Widget );
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.js
deleted file mode 100644 (file)
index ae1ec90..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-( function () {
-       /**
-        * Save filters widget. This widget is displayed in the tag area
-        * and allows the user to save the current state of the system
-        * as a new saved filter query they can later load or set as
-        * default.
-        *
-        * @extends OO.ui.PopupButtonWidget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget = function MwRcfiltersUiSaveFiltersPopupButtonWidget( controller, model, config ) {
-               var layout,
-                       checkBoxLayout,
-                       $popupContent = $( '<div>' );
-
-               config = config || {};
-
-               this.controller = controller;
-               this.model = model;
-
-               // Parent
-               mw.rcfilters.ui.SaveFiltersPopupButtonWidget.parent.call( this, $.extend( {
-                       framed: false,
-                       icon: 'bookmark',
-                       title: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
-                       popup: {
-                               classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup' ],
-                               padded: true,
-                               head: true,
-                               label: mw.msg( 'rcfilters-savedqueries-add-new-title' ),
-                               $content: $popupContent
-                       }
-               }, config ) );
-               // // HACK: Add an icon to the popup head label
-               this.popup.$head.prepend( ( new OO.ui.IconWidget( { icon: 'bookmark' } ) ).$element );
-
-               this.input = new OO.ui.TextInputWidget( {
-                       placeholder: mw.msg( 'rcfilters-savedqueries-new-name-placeholder' )
-               } );
-               layout = new OO.ui.FieldLayout( this.input, {
-                       label: mw.msg( 'rcfilters-savedqueries-new-name-label' ),
-                       align: 'top'
-               } );
-
-               this.setAsDefaultCheckbox = new OO.ui.CheckboxInputWidget();
-               checkBoxLayout = new OO.ui.FieldLayout( this.setAsDefaultCheckbox, {
-                       label: mw.msg( 'rcfilters-savedqueries-setdefault' ),
-                       align: 'inline'
-               } );
-
-               this.applyButton = new OO.ui.ButtonWidget( {
-                       label: mw.msg( 'rcfilters-savedqueries-apply-label' ),
-                       classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-apply' ],
-                       flags: [ 'primary', 'progressive' ]
-               } );
-               this.cancelButton = new OO.ui.ButtonWidget( {
-                       label: mw.msg( 'rcfilters-savedqueries-cancel-label' ),
-                       classes: [ 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons-cancel' ]
-               } );
-
-               $popupContent
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-layout' )
-                                       .append( layout.$element ),
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-options' )
-                                       .append( checkBoxLayout.$element ),
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget-popup-buttons' )
-                                       .append(
-                                               this.cancelButton.$element,
-                                               this.applyButton.$element
-                                       )
-                       );
-
-               // Events
-               this.popup.connect( this, {
-                       ready: 'onPopupReady'
-               } );
-               this.input.connect( this, {
-                       change: 'onInputChange',
-                       enter: 'onInputEnter'
-               } );
-               this.input.$input.on( {
-                       keyup: this.onInputKeyup.bind( this )
-               } );
-               this.setAsDefaultCheckbox.connect( this, { change: 'onSetAsDefaultChange' } );
-               this.cancelButton.connect( this, { click: 'onCancelButtonClick' } );
-               this.applyButton.connect( this, { click: 'onApplyButtonClick' } );
-
-               // Initialize
-               this.applyButton.setDisabled( !this.input.getValue() );
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-saveFiltersPopupButtonWidget' );
-       };
-
-       /* Initialization */
-       OO.inheritClass( mw.rcfilters.ui.SaveFiltersPopupButtonWidget, OO.ui.PopupButtonWidget );
-
-       /**
-        * Respond to input enter event
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onInputEnter = function () {
-               this.apply();
-       };
-
-       /**
-        * Respond to input change event
-        *
-        * @param {string} value Input value
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onInputChange = function ( value ) {
-               value = value.trim();
-
-               this.applyButton.setDisabled( !value );
-       };
-
-       /**
-        * Respond to input keyup event, this is the way to intercept 'escape' key
-        *
-        * @param {jQuery.Event} e Event data
-        * @return {boolean} false
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onInputKeyup = function ( e ) {
-               if ( e.which === OO.ui.Keys.ESCAPE ) {
-                       this.popup.toggle( false );
-                       return false;
-               }
-       };
-
-       /**
-        * Respond to popup ready event
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onPopupReady = function () {
-               this.input.focus();
-       };
-
-       /**
-        * Respond to "set as default" checkbox change
-        * @param {boolean} checked State of the checkbox
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onSetAsDefaultChange = function ( checked ) {
-               var messageKey = checked ?
-                       'rcfilters-savedqueries-apply-and-setdefault-label' :
-                       'rcfilters-savedqueries-apply-label';
-
-               this.applyButton
-                       .setIcon( checked ? 'pushPin' : null )
-                       .setLabel( mw.msg( messageKey ) );
-       };
-
-       /**
-        * Respond to cancel button click event
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onCancelButtonClick = function () {
-               this.popup.toggle( false );
-       };
-
-       /**
-        * Respond to apply button click event
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.onApplyButtonClick = function () {
-               this.apply();
-       };
-
-       /**
-        * Apply and add the new quick link
-        */
-       mw.rcfilters.ui.SaveFiltersPopupButtonWidget.prototype.apply = function () {
-               var label = this.input.getValue().trim();
-
-               // This condition is more for sanity-check, since the
-               // apply button should be disabled if the label is empty
-               if ( label ) {
-                       this.controller.saveCurrentQuery( label, this.setAsDefaultCheckbox.isSelected() );
-                       this.input.setValue( '' );
-                       this.setAsDefaultCheckbox.setSelected( false );
-                       this.popup.toggle( false );
-
-                       this.emit( 'saveCurrent' );
-               }
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListItemWidget.js
deleted file mode 100644 (file)
index f1364d1..0000000
+++ /dev/null
@@ -1,331 +0,0 @@
-( function () {
-       /**
-        * Quick links menu option widget
-        *
-        * @class
-        * @extends OO.ui.Widget
-        * @mixins OO.ui.mixin.LabelElement
-        * @mixins OO.ui.mixin.IconElement
-        * @mixins OO.ui.mixin.TitledElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.SavedQueryItemModel} model View model
-        * @param {Object} [config] Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget = function MwRcfiltersUiSavedLinksListWidget( model, config ) {
-               config = config || {};
-
-               this.model = model;
-
-               // Parent
-               mw.rcfilters.ui.SavedLinksListItemWidget.parent.call( this, $.extend( {
-                       data: this.model.getID()
-               }, config ) );
-
-               // Mixin constructors
-               OO.ui.mixin.LabelElement.call( this, $.extend( {
-                       label: this.model.getLabel()
-               }, config ) );
-               OO.ui.mixin.IconElement.call( this, $.extend( {
-                       icon: ''
-               }, config ) );
-               OO.ui.mixin.TitledElement.call( this, $.extend( {
-                       title: this.model.getLabel()
-               }, config ) );
-
-               this.edit = false;
-               this.$overlay = config.$overlay || this.$element;
-
-               this.popupButton = new OO.ui.ButtonWidget( {
-                       classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-button' ],
-                       icon: 'ellipsis',
-                       framed: false
-               } );
-               this.menu = new OO.ui.MenuSelectWidget( {
-                       classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-menu' ],
-                       widget: this.popupButton,
-                       width: 200,
-                       horizontalPosition: 'end',
-                       $floatableContainer: this.popupButton.$element,
-                       items: [
-                               new OO.ui.MenuOptionWidget( {
-                                       data: 'edit',
-                                       icon: 'edit',
-                                       label: mw.msg( 'rcfilters-savedqueries-rename' )
-                               } ),
-                               new OO.ui.MenuOptionWidget( {
-                                       data: 'delete',
-                                       icon: 'trash',
-                                       label: mw.msg( 'rcfilters-savedqueries-remove' )
-                               } ),
-                               new OO.ui.MenuOptionWidget( {
-                                       data: 'default',
-                                       icon: 'pushPin',
-                                       label: mw.msg( 'rcfilters-savedqueries-setdefault' )
-                               } )
-                       ]
-               } );
-
-               this.editInput = new OO.ui.TextInputWidget( {
-                       classes: [ 'mw-rcfilters-ui-savedLinksListItemWidget-input' ]
-               } );
-               this.saveButton = new OO.ui.ButtonWidget( {
-                       icon: 'check',
-                       flags: [ 'primary', 'progressive' ]
-               } );
-               this.toggleEdit( false );
-
-               // Events
-               this.model.connect( this, { update: 'onModelUpdate' } );
-               this.popupButton.connect( this, { click: 'onPopupButtonClick' } );
-               this.menu.connect( this, {
-                       choose: 'onMenuChoose'
-               } );
-               this.saveButton.connect( this, { click: 'save' } );
-               this.editInput.connect( this, {
-                       change: 'onInputChange',
-                       enter: 'save'
-               } );
-               this.editInput.$input.on( {
-                       blur: this.onInputBlur.bind( this ),
-                       keyup: this.onInputKeyup.bind( this )
-               } );
-               this.$element.on( { click: this.onClick.bind( this ) } );
-               this.$label.on( { click: this.onClick.bind( this ) } );
-               this.$icon.on( { click: this.onDefaultIconClick.bind( this ) } );
-               // Prevent propagation on mousedown for the save button
-               // so the menu doesn't close
-               this.saveButton.$element.on( { mousedown: function () {
-                       return false;
-               } } );
-
-               // Initialize
-               this.toggleDefault( !!this.model.isDefault() );
-               this.$overlay.append( this.menu.$element );
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget' )
-                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-query-' + this.model.getID() )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-table' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-row' )
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-content' )
-                                                                       .append(
-                                                                               this.$label
-                                                                                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-label' ),
-                                                                               this.editInput.$element,
-                                                                               this.saveButton.$element
-                                                                       ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                                       .addClass( 'mw-rcfilters-ui-savedLinksListItemWidget-icon' )
-                                                                       .append( this.$icon ),
-                                                               this.popupButton.$element
-                                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                       )
-                                       )
-                       );
-       };
-
-       /* Initialization */
-       OO.inheritClass( mw.rcfilters.ui.SavedLinksListItemWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.SavedLinksListItemWidget, OO.ui.mixin.LabelElement );
-       OO.mixinClass( mw.rcfilters.ui.SavedLinksListItemWidget, OO.ui.mixin.IconElement );
-       OO.mixinClass( mw.rcfilters.ui.SavedLinksListItemWidget, OO.ui.mixin.TitledElement );
-
-       /* Events */
-
-       /**
-        * @event delete
-        *
-        * The delete option was selected for this item
-        */
-
-       /**
-        * @event default
-        * @param {boolean} default Item is default
-        *
-        * The 'make default' option was selected for this item
-        */
-
-       /**
-        * @event edit
-        * @param {string} newLabel New label for the query
-        *
-        * The label has been edited
-        */
-
-       /* Methods */
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onModelUpdate = function () {
-               this.setLabel( this.model.getLabel() );
-               this.toggleDefault( this.model.isDefault() );
-       };
-
-       /**
-        * Respond to click on the element or label
-        *
-        * @fires click
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onClick = function () {
-               if ( !this.editing ) {
-                       this.emit( 'click' );
-               }
-       };
-
-       /**
-        * Respond to click on the 'default' icon. Open the submenu where the
-        * default state can be changed.
-        *
-        * @return {boolean} false
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onDefaultIconClick = function () {
-               this.menu.toggle();
-               return false;
-       };
-
-       /**
-        * Respond to popup button click event
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onPopupButtonClick = function () {
-               this.menu.toggle();
-       };
-
-       /**
-        * Respond to menu choose event
-        *
-        * @param {OO.ui.MenuOptionWidget} item Chosen item
-        * @fires delete
-        * @fires default
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onMenuChoose = function ( item ) {
-               var action = item.getData();
-
-               if ( action === 'edit' ) {
-                       this.toggleEdit( true );
-               } else if ( action === 'delete' ) {
-                       this.emit( 'delete' );
-               } else if ( action === 'default' ) {
-                       this.emit( 'default', !this.default );
-               }
-               // Reset selected
-               this.menu.selectItem( null );
-               // Close the menu
-               this.menu.toggle( false );
-       };
-
-       /**
-        * Respond to input keyup event, this is the way to intercept 'escape' key
-        *
-        * @param {jQuery.Event} e Event data
-        * @return {boolean} false
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onInputKeyup = function ( e ) {
-               if ( e.which === OO.ui.Keys.ESCAPE ) {
-                       // Return the input to the original label
-                       this.editInput.setValue( this.getLabel() );
-                       this.toggleEdit( false );
-                       return false;
-               }
-       };
-
-       /**
-        * Respond to blur event on the input
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onInputBlur = function () {
-               this.save();
-
-               // Whether the save succeeded or not, the input-blur event
-               // means we need to cancel editing mode
-               this.toggleEdit( false );
-       };
-
-       /**
-        * Respond to input change event
-        *
-        * @param {string} value Input value
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.onInputChange = function ( value ) {
-               value = value.trim();
-
-               this.saveButton.setDisabled( !value );
-       };
-
-       /**
-        * Save the name of the query
-        *
-        * @param {string} [value] The value to save
-        * @fires edit
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.save = function () {
-               var value = this.editInput.getValue().trim();
-
-               if ( value ) {
-                       this.emit( 'edit', value );
-                       this.toggleEdit( false );
-               }
-       };
-
-       /**
-        * Toggle edit mode on this widget
-        *
-        * @param {boolean} isEdit Widget is in edit mode
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.toggleEdit = function ( isEdit ) {
-               isEdit = isEdit === undefined ? !this.editing : isEdit;
-
-               if ( this.editing !== isEdit ) {
-                       this.$element.toggleClass( 'mw-rcfilters-ui-savedLinksListItemWidget-edit', isEdit );
-                       this.editInput.setValue( this.getLabel() );
-
-                       this.editInput.toggle( isEdit );
-                       this.$label.toggleClass( 'oo-ui-element-hidden', isEdit );
-                       this.$icon.toggleClass( 'oo-ui-element-hidden', isEdit );
-                       this.popupButton.toggle( !isEdit );
-                       this.saveButton.toggle( isEdit );
-
-                       if ( isEdit ) {
-                               this.editInput.$input.trigger( 'focus' );
-                       }
-                       this.editing = isEdit;
-               }
-       };
-
-       /**
-        * Toggle default this widget
-        *
-        * @param {boolean} isDefault This item is default
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.toggleDefault = function ( isDefault ) {
-               isDefault = isDefault === undefined ? !this.default : isDefault;
-
-               if ( this.default !== isDefault ) {
-                       this.default = isDefault;
-                       this.setIcon( this.default ? 'pushPin' : '' );
-                       this.menu.findItemFromData( 'default' ).setLabel(
-                               this.default ?
-                                       mw.msg( 'rcfilters-savedqueries-unsetdefault' ) :
-                                       mw.msg( 'rcfilters-savedqueries-setdefault' )
-                       );
-               }
-       };
-
-       /**
-        * Get item ID
-        *
-        * @return {string} Query identifier
-        */
-       mw.rcfilters.ui.SavedLinksListItemWidget.prototype.getID = function () {
-               return this.model.getID();
-       };
-
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js
deleted file mode 100644 (file)
index b4ec781..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-( function () {
-       /**
-        * Quick links widget
-        *
-        * @class
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.SavedQueriesModel} model View model
-        * @param {Object} [config] Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.SavedLinksListWidget = function MwRcfiltersUiSavedLinksListWidget( controller, model, config ) {
-               var $labelNoEntries = $( '<div>' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-title' )
-                                       .text( mw.msg( 'rcfilters-quickfilters-placeholder-title' ) ),
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-savedLinksListWidget-placeholder-description' )
-                                       .text( mw.msg( 'rcfilters-quickfilters-placeholder-description' ) )
-                       );
-
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.SavedLinksListWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-               this.$overlay = config.$overlay || this.$element;
-
-               this.placeholderItem = new OO.ui.DecoratedOptionWidget( {
-                       classes: [ 'mw-rcfilters-ui-savedLinksListWidget-placeholder' ],
-                       label: $labelNoEntries,
-                       icon: 'bookmark'
-               } );
-
-               this.menu = new mw.rcfilters.ui.GroupWidget( {
-                       events: {
-                               click: 'menuItemClick',
-                               delete: 'menuItemDelete',
-                               default: 'menuItemDefault',
-                               edit: 'menuItemEdit'
-                       },
-                       classes: [ 'mw-rcfilters-ui-savedLinksListWidget-menu' ],
-                       items: [ this.placeholderItem ]
-               } );
-               this.button = new OO.ui.PopupButtonWidget( {
-                       classes: [ 'mw-rcfilters-ui-savedLinksListWidget-button' ],
-                       label: mw.msg( 'rcfilters-quickfilters' ),
-                       icon: 'bookmark',
-                       indicator: 'down',
-                       $overlay: this.$overlay,
-                       popup: {
-                               width: 300,
-                               anchor: false,
-                               align: 'backwards',
-                               $autoCloseIgnore: this.$overlay,
-                               $content: this.menu.$element
-                       }
-               } );
-
-               // Events
-               this.model.connect( this, {
-                       add: 'onModelAddItem',
-                       remove: 'onModelRemoveItem'
-               } );
-               this.menu.connect( this, {
-                       menuItemClick: 'onMenuItemClick',
-                       menuItemDelete: 'onMenuItemRemove',
-                       menuItemDefault: 'onMenuItemDefault',
-                       menuItemEdit: 'onMenuItemEdit'
-               } );
-
-               this.placeholderItem.toggle( this.model.isEmpty() );
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-savedLinksListWidget' )
-                       .append( this.button.$element );
-       };
-
-       /* Initialization */
-       OO.inheritClass( mw.rcfilters.ui.SavedLinksListWidget, OO.ui.Widget );
-
-       /* Methods */
-
-       /**
-        * Respond to menu item click event
-        *
-        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
-        */
-       mw.rcfilters.ui.SavedLinksListWidget.prototype.onMenuItemClick = function ( item ) {
-               this.controller.applySavedQuery( item.getID() );
-               this.button.popup.toggle( false );
-       };
-
-       /**
-        * Respond to menu item remove event
-        *
-        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
-        */
-       mw.rcfilters.ui.SavedLinksListWidget.prototype.onMenuItemRemove = function ( item ) {
-               this.controller.removeSavedQuery( item.getID() );
-       };
-
-       /**
-        * Respond to menu item default event
-        *
-        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
-        * @param {boolean} isDefault Item is default
-        */
-       mw.rcfilters.ui.SavedLinksListWidget.prototype.onMenuItemDefault = function ( item, isDefault ) {
-               this.controller.setDefaultSavedQuery( isDefault ? item.getID() : null );
-       };
-
-       /**
-        * Respond to menu item edit event
-        *
-        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
-        * @param {string} newLabel New label
-        */
-       mw.rcfilters.ui.SavedLinksListWidget.prototype.onMenuItemEdit = function ( item, newLabel ) {
-               this.controller.renameSavedQuery( item.getID(), newLabel );
-       };
-
-       /**
-        * Respond to menu add item event
-        *
-        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
-        */
-       mw.rcfilters.ui.SavedLinksListWidget.prototype.onModelAddItem = function ( item ) {
-               if ( this.menu.findItemFromData( item.getID() ) ) {
-                       return;
-               }
-
-               this.menu.addItems( [
-                       new mw.rcfilters.ui.SavedLinksListItemWidget( item, { $overlay: this.$overlay } )
-               ] );
-               this.placeholderItem.toggle( this.model.isEmpty() );
-       };
-
-       /**
-        * Respond to menu remove item event
-        *
-        * @param {mw.rcfilters.ui.SavedLinksListItemWidget} item Menu item
-        */
-       mw.rcfilters.ui.SavedLinksListWidget.prototype.onModelRemoveItem = function ( item ) {
-               this.menu.removeItems( [ this.menu.findItemFromData( item.getID() ) ] );
-               this.placeholderItem.toggle( this.model.isEmpty() );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js
deleted file mode 100644 (file)
index 88117e7..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-( function () {
-       /**
-        * Extend OOUI's TagItemWidget to also display a popup on hover.
-        *
-        * @class
-        * @extends OO.ui.TagItemWidget
-        * @mixins OO.ui.mixin.PopupElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel
-        * @param {mw.rcfilters.dm.FilterItem} invertModel
-        * @param {mw.rcfilters.dm.FilterItem} itemModel Item model
-        * @param {Object} config Configuration object
-        * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
-        */
-       mw.rcfilters.ui.TagItemWidget = function MwRcfiltersUiTagItemWidget(
-               controller, filtersViewModel, invertModel, itemModel, config
-       ) {
-               // Configuration initialization
-               config = config || {};
-
-               this.controller = controller;
-               this.invertModel = invertModel;
-               this.filtersViewModel = filtersViewModel;
-               this.itemModel = itemModel;
-               this.selected = false;
-
-               mw.rcfilters.ui.TagItemWidget.parent.call( this, $.extend( {
-                       data: this.itemModel.getName()
-               }, config ) );
-
-               this.$overlay = config.$overlay || this.$element;
-               this.popupLabel = new OO.ui.LabelWidget();
-
-               // Mixin constructors
-               OO.ui.mixin.PopupElement.call( this, $.extend( {
-                       popup: {
-                               padded: false,
-                               align: 'center',
-                               position: 'above',
-                               $content: $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-tagItemWidget-popup-content' )
-                                       .append( this.popupLabel.$element ),
-                               $floatableContainer: this.$element,
-                               classes: [ 'mw-rcfilters-ui-tagItemWidget-popup' ]
-                       }
-               }, config ) );
-
-               this.popupTimeoutShow = null;
-               this.popupTimeoutHide = null;
-
-               this.$highlight = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-tagItemWidget-highlight' );
-
-               // Add title attribute with the item label to 'x' button
-               this.closeButton.setTitle( mw.msg( 'rcfilters-tag-remove', this.itemModel.getLabel() ) );
-
-               // Events
-               this.filtersViewModel.connect( this, { highlightChange: 'updateUiBasedOnState' } );
-               this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
-               this.itemModel.connect( this, { update: 'updateUiBasedOnState' } );
-
-               // Initialization
-               this.$overlay.append( this.popup.$element );
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-tagItemWidget' )
-                       .prepend( this.$highlight )
-                       .attr( 'aria-haspopup', 'true' )
-                       .on( 'mouseenter', this.onMouseEnter.bind( this ) )
-                       .on( 'mouseleave', this.onMouseLeave.bind( this ) );
-
-               this.updateUiBasedOnState();
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.TagItemWidget, OO.ui.TagItemWidget );
-       OO.mixinClass( mw.rcfilters.ui.TagItemWidget, OO.ui.mixin.PopupElement );
-
-       /* Methods */
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.updateUiBasedOnState = function () {
-               // Update label if needed
-               var labelMsg = this.itemModel.getLabelMessageKey( this.invertModel.isSelected() );
-               if ( labelMsg ) {
-                       this.setLabel( $( '<div>' ).append(
-                               $( '<bdi>' ).html(
-                                       mw.message( labelMsg, mw.html.escape( this.itemModel.getLabel() ) ).parse()
-                               )
-                       ).contents() );
-               } else {
-                       this.setLabel(
-                               $( '<bdi>' ).append(
-                                       this.itemModel.getLabel()
-                               )
-                       );
-               }
-
-               this.setCurrentMuteState();
-               this.setHighlightColor();
-       };
-
-       /**
-        * Set the current highlight color for this item
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.setHighlightColor = function () {
-               var selectedColor = this.filtersViewModel.isHighlightEnabled() && this.itemModel.isHighlighted ?
-                       this.itemModel.getHighlightColor() :
-                       null;
-
-               this.$highlight
-                       .attr( 'data-color', selectedColor )
-                       .toggleClass(
-                               'mw-rcfilters-ui-tagItemWidget-highlight-highlighted',
-                               !!selectedColor
-                       );
-       };
-
-       /**
-        * Set the current mute state for this item
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.setCurrentMuteState = function () {};
-
-       /**
-        * Respond to mouse enter event
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.onMouseEnter = function () {
-               var labelText = this.itemModel.getStateMessage();
-
-               if ( labelText ) {
-                       this.popupLabel.setLabel( labelText );
-
-                       // Set timeout for the popup to show
-                       this.popupTimeoutShow = setTimeout( function () {
-                               this.popup.toggle( true );
-                       }.bind( this ), 500 );
-
-                       // Cancel the hide timeout
-                       clearTimeout( this.popupTimeoutHide );
-                       this.popupTimeoutHide = null;
-               }
-       };
-
-       /**
-        * Respond to mouse leave event
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.onMouseLeave = function () {
-               this.popupTimeoutHide = setTimeout( function () {
-                       this.popup.toggle( false );
-               }.bind( this ), 250 );
-
-               // Clear the show timeout
-               clearTimeout( this.popupTimeoutShow );
-               this.popupTimeoutShow = null;
-       };
-
-       /**
-        * Set selected state on this widget
-        *
-        * @param {boolean} [isSelected] Widget is selected
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.toggleSelected = function ( isSelected ) {
-               isSelected = isSelected !== undefined ? isSelected : !this.selected;
-
-               if ( this.selected !== isSelected ) {
-                       this.selected = isSelected;
-
-                       this.$element.toggleClass( 'mw-rcfilters-ui-tagItemWidget-selected', this.selected );
-               }
-       };
-
-       /**
-        * Get the selected state of this widget
-        *
-        * @return {boolean} Tag is selected
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.isSelected = function () {
-               return this.selected;
-       };
-
-       /**
-        * Get item name
-        *
-        * @return {string} Filter name
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.getName = function () {
-               return this.itemModel.getName();
-       };
-
-       /**
-        * Get item model
-        *
-        * @return {string} Filter model
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.getModel = function () {
-               return this.itemModel;
-       };
-
-       /**
-        * Get item view
-        *
-        * @return {string} Filter view
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.getView = function () {
-               return this.itemModel.getGroupModel().getView();
-       };
-
-       /**
-        * Remove and destroy external elements of this widget
-        */
-       mw.rcfilters.ui.TagItemWidget.prototype.destroy = function () {
-               // Destroy the popup
-               this.popup.$element.detach();
-
-               // Disconnect events
-               this.itemModel.disconnect( this );
-               this.closeButton.disconnect( this );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js
deleted file mode 100644 (file)
index e65abf2..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-( function () {
-       /**
-        * Widget defining the behavior used to choose from a set of values
-        * in a single_value group
-        *
-        * @class
-        * @extends OO.ui.Widget
-        * @mixins OO.ui.mixin.LabelElement
-        *
-        * @constructor
-        * @param {mw.rcfilters.dm.FilterGroup} model Group model
-        * @param {Object} [config] Configuration object
-        * @cfg {Function} [itemFilter] A filter function for the items from the
-        *  model. If not given, all items will be included. The function must
-        *  handle item models and return a boolean whether the item is included
-        *  or not. Example: function ( itemModel ) { return itemModel.isSelected(); }
-        */
-       mw.rcfilters.ui.ValuePickerWidget = function MwRcfiltersUiValuePickerWidget( model, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.ValuePickerWidget.parent.call( this, config );
-               // Mixin constructors
-               OO.ui.mixin.LabelElement.call( this, config );
-
-               this.model = model;
-               this.itemFilter = config.itemFilter || function () {
-                       return true;
-               };
-
-               // Build the selection from the item models
-               this.selectWidget = new OO.ui.ButtonSelectWidget();
-               this.initializeSelectWidget();
-
-               // Events
-               this.model.connect( this, { update: 'onModelUpdate' } );
-               this.selectWidget.connect( this, { choose: 'onSelectWidgetChoose' } );
-
-               // Initialize
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-valuePickerWidget' )
-                       .append(
-                               this.$label
-                                       .addClass( 'mw-rcfilters-ui-valuePickerWidget-title' ),
-                               this.selectWidget.$element
-                       );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.ValuePickerWidget, OO.ui.Widget );
-       OO.mixinClass( mw.rcfilters.ui.ValuePickerWidget, OO.ui.mixin.LabelElement );
-
-       /* Events */
-
-       /**
-        * @event choose
-        * @param {string} name Item name
-        *
-        * An item has been chosen
-        */
-
-       /* Methods */
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.ValuePickerWidget.prototype.onModelUpdate = function () {
-               this.selectCurrentModelItem();
-       };
-
-       /**
-        * Respond to select widget choose event
-        *
-        * @param {OO.ui.ButtonOptionWidget} chosenItem Chosen item
-        * @fires choose
-        */
-       mw.rcfilters.ui.ValuePickerWidget.prototype.onSelectWidgetChoose = function ( chosenItem ) {
-               this.emit( 'choose', chosenItem.getData() );
-       };
-
-       /**
-        * Initialize the select widget
-        */
-       mw.rcfilters.ui.ValuePickerWidget.prototype.initializeSelectWidget = function () {
-               var items = this.model.getItems()
-                       .filter( this.itemFilter )
-                       .map( function ( filterItem ) {
-                               return new OO.ui.ButtonOptionWidget( {
-                                       data: filterItem.getName(),
-                                       label: filterItem.getLabel()
-                               } );
-                       } );
-
-               this.selectWidget.clearItems();
-               this.selectWidget.addItems( items );
-
-               this.selectCurrentModelItem();
-       };
-
-       /**
-        * Select the current item that corresponds with the model item
-        * that is currently selected
-        */
-       mw.rcfilters.ui.ValuePickerWidget.prototype.selectCurrentModelItem = function () {
-               var selectedItem = this.model.findSelectedItems()[ 0 ];
-
-               if ( selectedItem ) {
-                       this.selectWidget.selectItemByData( selectedItem.getName() );
-               }
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ViewSwitchWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ViewSwitchWidget.js
deleted file mode 100644 (file)
index 72d2203..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-( function () {
-       /**
-        * A widget for the footer for the default view, allowing to switch views
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller Controller
-        * @param {mw.rcfilters.dm.FiltersViewModel} model View model
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.ViewSwitchWidget = function MwRcfiltersUiViewSwitchWidget( controller, model, config ) {
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.ViewSwitchWidget.parent.call( this, config );
-
-               this.controller = controller;
-               this.model = model;
-
-               this.buttons = new mw.rcfilters.ui.GroupWidget( {
-                       events: {
-                               click: 'buttonClick'
-                       },
-                       items: [
-                               new OO.ui.ButtonWidget( {
-                                       data: 'namespaces',
-                                       icon: 'article',
-                                       label: mw.msg( 'namespaces' )
-                               } ),
-                               new OO.ui.ButtonWidget( {
-                                       data: 'tags',
-                                       icon: 'tag',
-                                       label: mw.msg( 'rcfilters-view-tags' )
-                               } )
-                       ]
-               } );
-
-               // Events
-               this.model.connect( this, { update: 'onModelUpdate' } );
-               this.buttons.connect( this, { buttonClick: 'onButtonClick' } );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-viewSwitchWidget' )
-                       .append(
-                               new OO.ui.LabelWidget( {
-                                       label: mw.msg( 'rcfilters-advancedfilters' )
-                               } ).$element,
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-viewSwitchWidget-buttons' )
-                                       .append( this.buttons.$element )
-                       );
-       };
-
-       /* Initialize */
-
-       OO.inheritClass( mw.rcfilters.ui.ViewSwitchWidget, OO.ui.Widget );
-
-       /**
-        * Respond to model update event
-        */
-       mw.rcfilters.ui.ViewSwitchWidget.prototype.onModelUpdate = function () {
-               var currentView = this.model.getCurrentView();
-
-               this.buttons.getItems().forEach( function ( buttonWidget ) {
-                       buttonWidget.setActive( buttonWidget.getData() === currentView );
-               } );
-       };
-
-       /**
-        * Respond to button switch click
-        *
-        * @param {OO.ui.ButtonWidget} buttonWidget Clicked button
-        */
-       mw.rcfilters.ui.ViewSwitchWidget.prototype.onButtonClick = function ( buttonWidget ) {
-               this.controller.switchView( buttonWidget.getData() );
-       };
-}() );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js
deleted file mode 100644 (file)
index 423c105..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-( function () {
-       /**
-        * Top section (between page title and filters) on Special:Watchlist
-        *
-        * @extends OO.ui.Widget
-        *
-        * @constructor
-        * @param {mw.rcfilters.Controller} controller
-        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
-        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
-        * @param {jQuery} $watchlistDetails Content of the 'details' section that includes watched pages count
-        * @param {Object} [config] Configuration object
-        */
-       mw.rcfilters.ui.WatchlistTopSectionWidget = function MwRcfiltersUiWatchlistTopSectionWidget(
-               controller, changesListModel, savedLinksListWidget, $watchlistDetails, config
-       ) {
-               var editWatchlistButton,
-                       markSeenButton,
-                       $topTable,
-                       $bottomTable,
-                       $separator;
-               config = config || {};
-
-               // Parent
-               mw.rcfilters.ui.WatchlistTopSectionWidget.parent.call( this, config );
-
-               editWatchlistButton = new OO.ui.ButtonWidget( {
-                       label: mw.msg( 'rcfilters-watchlist-edit-watchlist-button' ),
-                       icon: 'edit',
-                       href: mw.config.get( 'wgStructuredChangeFiltersEditWatchlistUrl' )
-               } );
-               markSeenButton = new mw.rcfilters.ui.MarkSeenButtonWidget( controller, changesListModel );
-
-               $topTable = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-table' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-row' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails' )
-                                                       .append( $watchlistDetails )
-                                       )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton' )
-                                                       .append( editWatchlistButton.$element )
-                                       )
-                       );
-
-               $bottomTable = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-table' )
-                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinksTable' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-row' )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                       .append( markSeenButton.$element )
-                                       )
-                                       .append(
-                                               $( '<div>' )
-                                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' )
-                                                       .append( savedLinksListWidget.$element )
-                                       )
-                       );
-
-               $separator = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-separator' );
-
-               this.$element
-                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget' )
-                       .append( $topTable, $separator, $bottomTable );
-       };
-
-       /* Initialization */
-
-       OO.inheritClass( mw.rcfilters.ui.WatchlistTopSectionWidget, OO.ui.Widget );
-}() );
index e27a5f5..f62afb5 100644 (file)
@@ -1,8 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.467808,-258.39005)">
-<path style="opacity:1;fill:#3366bb;" d="m43.47,259.4-3,3-3,0,0,4,3,0,3,3zm-1,2.5,0,5-1.5-1.5-2.5,0,0-2,2.5,0z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m43.9,262.5c0-0.6213,0.6213-1.243,1.243-0.6213,0,0,0.6213,0.6213,0.6213,2.485s-0.6213,2.485-0.6213,2.485c-0.6213,0.6213-1.243,0-1.243-0.6213,0,0,0.6213-0.6213,0.6213-1.864s-0.6213-1.864-0.6213-1.864z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m45.76,261.2c0-0.6213,0.6213-1.243,1.243-0.6213,0,0,1.243,1.243,1.243,3.728s-1.243,3.728-1.243,3.728c-0.6213,0.6213-1.243,0-1.243-0.6213,0,0,1.243-1.243,1.243-3.107s-1.243-3.107-1.243-3.107z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#36b" d="M7.002 1.01l-3 3h-3v4h3l3 3zm-1 2.5v5l-1.5-1.5h-2.5v-2h2.5z"/>
+       <path fill="#15a5ea" d="M7.432 4.11c0-.621.621-1.243 1.243-.621 0 0 .621.621.621 2.485s-.62 2.485-.62 2.485c-.622.621-1.244 0-1.244-.622 0 0 .621-.62.621-1.864s-.62-1.864-.62-1.864z"/>
+       <path fill="#15a5ea" d="M9.292 2.81c0-.621.621-1.243 1.243-.621 0 0 1.243 1.243 1.243 3.728s-1.243 3.728-1.243 3.728c-.621.621-1.243 0-1.243-.622 0 0 1.243-1.243 1.243-3.107S9.292 2.81 9.292 2.81z"/>
 </svg>
index 683bbcd..28cf408 100644 (file)
@@ -1,8 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.467808,-258.39005)">
-<path style="opacity:1;fill:#3366bb;" d="m42.47,259.4,3,3,3,0,0,4-3,0-3,3zm1,2.5,0,5,1.5-1.5,2.5,0,0-2-2.5,0z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m42.04,262.5c0-0.6213-0.6213-1.243-1.243-0.6213,0,0-0.6213,0.6213-0.6213,2.485s0.6213,2.485,0.6213,2.485c0.6213,0.6213,1.243,0,1.243-0.6213,0,0-0.6213-0.6213-0.6213-1.864s0.6213-1.864,0.6213-1.864z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m40.17,261.2c0-0.6213-0.6213-1.243-1.243-0.6213,0,0-1.243,1.243-1.243,3.728s1.243,3.728,1.243,3.728c0.6213,0.6213,1.243,0,1.243-0.6213,0,0-1.243-1.243-1.243-3.107s1.243-3.107,1.243-3.107z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#36b" d="M6.002 1.01l3 3h3v4h-3l-3 3zm1 2.5v5l1.5-1.5h2.5v-2h-2.5z"/>
+       <path fill="#15a5ea" d="M5.572 4.11c0-.621-.621-1.243-1.243-.621 0 0-.621.621-.621 2.485s.621 2.485.621 2.485c.621.621 1.243 0 1.243-.622 0 0-.621-.62-.621-1.864s.621-1.864.621-1.864zm-1.87-1.3c0-.621-.621-1.243-1.243-.621 0 0-1.243 1.243-1.243 3.728S2.46 9.645 2.46 9.645c.621.621 1.243 0 1.243-.622 0 0-1.243-1.243-1.243-3.107S3.702 2.81 3.702 2.81z"/>
 </svg>
index bd5329e..07d285e 100644 (file)
@@ -1,6 +1,4 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.467808,-258.39005)">
-<path style="opacity:1;fill:#3366bb;" d="m38.09,260.4-0.6213,0.6213,0,5.757,0.6213,0.6213,1.689,0-0.6213,2.728,4.311-2.728,4.379,0,0.6213-0.6213,0-5.757-0.6213-0.6213zm0.3787,1,9,0,0,5-4,0-2.902,1.897,0.9021-1.897-3,0z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#36b" d="M1.622 2.01l-.621.621v5.757l.621.622h1.69l-.622 2.728L7 9.01h4.38l.62-.622V2.631l-.621-.621zm.379 1h9v5H7L4.099 9.907 5 8.01H2z"/>
 </svg>
index b86218f..a6f0262 100644 (file)
@@ -1,6 +1,4 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.467808,-258.39005)">
-<path style="opacity:1;fill:#3366bb;" d="m47.85,260.4,0.6213,0.6213,0,5.757-0.6213,0.6213-1.689,0,0.6213,2.728-4.311-2.728-4.379,0-0.6213-0.6213,0-5.757,0.6213-0.6213zm-0.3787,1-9,0,0,5,4,0,2.902,1.897-0.9021-1.897,3,0z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#36b" d="M11.382 2.01l.621.621v5.757l-.62.622h-1.69l.621 2.728-4.31-2.728h-4.38l-.62-.622V2.631l.62-.621zm-.379 1h-9v5h4l2.902 1.897-.902-1.897h3z"/>
 </svg>
index 4396098..ee318c6 100644 (file)
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<path style="opacity:1;fill:#15a5ea;" d="m7.5,2,0,3,2.5,0,1-1-2.5,0,0-3z"/>
-<path style="opacity:1;fill:#3366bb;" d="m3,1,0,10,8,0,0-7-2.5-3zm1,1,4,0,2,2.5,0,5.5-6,0z"/>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#15a5ea" d="M7.5 2v3H10l1-1H8.5V1z"/>
+       <path fill="#36b" d="M3 1v10h8V4L8.5 1zm1 1h4l2 2.5V10H4z"/>
 </svg>
index c37dadc..ddca3d4 100644 (file)
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<path style="opacity:1;fill:#15a5ea;" d="m5.5,2,0,3-2.5,0-1-1,2.5,0,0-3z"/>
-<path style="opacity:1;fill:#3366bb;" d="m10,1,0,10-8,0,0-7,2.5-3zm-1,1-4,0-2,2.5,0,5.5,6,0z"/>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#15a5ea" d="M5.5 2v3H3L2 4h2.5V1z"/>
+       <path fill="#36b" d="M10 1v10H2V4l2.5-3zM9 2H5L3 4.5V10h6z"/>
 </svg>
index e914b7d..ae47a02 100644 (file)
@@ -1,8 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.467808,-258.39005)">
-<path style="opacity:1;fill:#3366bb;" d="m41.47,259.4,7,0,0,7-2-2-3,2,0-1,3-2.25,1,1,0-3.75-3.75,0,1,1-2.25,3-1,0,2-3z"/>
-<path style="opacity:1;fill:#3366bb;" d="m43.47,261.4-5,0,0,8,8,0,0-5-1,0,0,4-6,0,0-6,4,0z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m40.55,263.9c0-0.6213,0.6213-0.6213,0.6213-0.6213,1.864,0.6213,3.107,1.864,3.728,3.728,0,0,0,0.6213-0.6213,0.6213-1.243-1.864-1.864-2.485-3.728-3.728z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#36b" d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
+       <path fill="#36b" d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
+       <path fill="#15a5ea" d="M4.082 5.51c0-.621.621-.621.621-.621 1.864.621 3.107 1.864 3.728 3.728 0 0 0 .621-.62.621-1.245-1.864-1.866-2.485-3.73-3.728z"/>
 </svg>
index 698c616..2a7827a 100644 (file)
@@ -1,8 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.467808,-258.39005)">
-<path style="opacity:1;fill:#3366bb;" d="m44.47,259.4-7,0,0,7,2-2,3,2,0-1-3-2.25-1,1,0-3.75,3.75,0-1,1,2.25,3,1,0-2-3z"/>
-<path style="opacity:1;fill:#3366bb;" d="m42.47,261.4,5,0,0,8-8,0,0-5,1,0,0,4,6,0,0-6-4,0z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m45.39,263.9c0-0.6213-0.6213-0.6213-0.6213-0.6213-1.864,0.6213-3.107,1.864-3.728,3.728l0.6213,0.6213c1.243-1.864,1.864-2.485,3.728-3.728z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#36b" d="M8.002 1.01h-7v7l2-2 3 2v-1l-3-2.25-1 1V2.01h3.75l-1 1 2.25 3h1l-2-3z"/>
+       <path fill="#36b" d="M6.002 3.01h5v8h-8v-5h1v4h6v-6h-4z"/>
+       <path fill="#15a5ea" d="M8.922 5.51c0-.621-.621-.621-.621-.621-1.864.621-3.107 1.864-3.728 3.728l.621.621C6.437 7.374 7.058 6.753 8.922 5.51z"/>
 </svg>
index 3a90c31..d8845aa 100644 (file)
@@ -1,9 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-136.35715,-374.43362)">
-<path style="opacity:1;fill:#3465ba;" d="m137.4,376.9,0,7.5,1-0.9872,0-6.013,2,0,2,1,4-0.0002,0,2,1,0,0-2.5-0.5-0.5002-4.5,0.0002-2-1-2.5,0z"/>
-<path style="opacity:1;fill:#16a4e8;" d="m142.4,379.4-1-0.0001-2.5,0-0.5,0.5-1,4.5,10-0.0001,0-4.5-0.5-0.5zm0,1,4-0.0001,0,3-7.75-0.0001,0.75-3,2,0z"/>
-<path style="opacity:1;fill:#3465ba;" d="m138.4,385.4,0,1,8,0,0-1z"/>
-<path style="opacity:1;fill:#3465ba;" d="m141.4,383.9,0,2,2,0,0-2c0-0.5-2-0.5-2,0z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#3465ba" d="M1.043 2.466v7.5l1-.987V2.966h2l2 1h4v2h1v-2.5l-.5-.5h-4.5l-2-1h-2.5z"/>
+       <path fill="#16a4e8" d="M6.043 4.966h-3.5l-.5.5-1 4.5h10v-4.5l-.5-.5zm0 1h4v3h-7.75l.75-3h2z"/>
+       <path fill="#3465ba" d="M2.043 10.966v1h8v-1z"/>
+       <path fill="#3465ba" d="M5.043 9.466v2h2v-2c0-.5-2-.5-2 0z"/>
 </svg>
index 29e4b44..f5c5c81 100644 (file)
@@ -1,9 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-136.35715,-374.43362)">
-<path style="opacity:1;fill:#3465ba;" d="m148.4,376.9,0,7.5-1-0.9872,0-6.013-2,0-2,1-4-0.0002,0,2-1,0,0-2.5,0.5-0.5002,4.5,0.0002,2-1,2.5,0z"/>
-<path style="opacity:1;fill:#16a4e8;" d="m143.4,379.4,1-0.0001,2.5,0,0.5,0.5,1,4.5-10-0.0001,0-4.5,0.5-0.5zm0,1-4-0.0001,0,3,7.75-0.0001-0.75-3-2,0z"/>
-<path style="opacity:1;fill:#3465ba;" d="m147.4,385.4,0,1-8,0,0-1z"/>
-<path style="opacity:1;fill:#3465ba;" d="m144.4,383.9,0,2-2,0,0-2c0-0.5,2-0.5,2,0z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#3465ba" d="M12.043 2.466v7.5l-1-.987V2.966h-2l-2 1h-4v2h-1v-2.5l.5-.5h4.5l2-1h2.5z"/>
+       <path fill="#16a4e8" d="M7.043 4.966h3.5l.5.5 1 4.5h-10v-4.5l.5-.5zm0 1h-4v3h7.75l-.75-3h-2z"/>
+       <path fill="#3465ba" d="M11.043 10.966v1h-8v-1z"/>
+       <path fill="#3465ba" d="M8.043 9.466v2h-2v-2c0-.5 2-.5 2 0z"/>
 </svg>
index 4d3dcb6..574635a 100644 (file)
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 15" width="15" height="11">
-    <g id="magnify-clip" fill="#fff" stroke="#000">
-        <path id="bigbox" d="M1.509 1.865h10.99v7.919h-10.99z"/>
-        <path id="smallbox" d="M-1.499 6.868h5.943v4.904h-5.943z"/>
-    </g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="15" height="11" viewBox="0 0 11 15">
+       <g id="magnify-clip" fill="#fff" stroke="#000">
+               <path id="bigbox" d="M1.509 1.865h10.99v7.919H1.509z"/>
+               <path id="smallbox" d="M-1.499 6.868h5.943v4.904h-5.943z"/>
+       </g>
 </svg>
index 582e4ae..31176d2 100644 (file)
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11 15" width="15" height="11">
-    <g id="magnify-clip" fill="#fff" stroke="#000">
-        <path id="bigbox" d="M9.491 1.865h-10.99v7.919h10.99z"/>
-        <path id="smallbox" d="M12.499 6.868h-5.943v4.904h5.943z"/>
-    </g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="15" height="11" viewBox="0 0 11 15">
+       <g id="magnify-clip" fill="#fff" stroke="#000">
+               <path id="bigbox" d="M9.491 1.865h-10.99v7.919h10.99z"/>
+               <path id="smallbox" d="M12.499 6.868H6.556v4.904h5.943z"/>
+       </g>
 </svg>
index 5e534fe..56ecc08 100644 (file)
@@ -1,7 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.47,-257.4)">
-<path style="fill:#15a5ea;" d="m38.31,261.4,4.66,4.349,4.66-4.349-0.6213-0.6213-1.243,0.6216-2.796,2.485-2.796-2.485-1.243-0.6216z"/>
-<path style="fill:#3366bb;" d="m37.47,260.4,0,8,11,0,0-8zm1,1,9,0,0,6-9,0z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#15a5ea" d="M1.84 4L6.5 8.349 11.16 4l-.621-.621L9.296 4 6.5 6.485 3.704 4 2.46 3.38z"/>
+       <path fill="#36b" d="M1 3v8h11V3zm1 1h9v6H2z"/>
 </svg>
index d52d0db..d683adc 100644 (file)
@@ -1,14 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="13" width="13" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
-<g transform="translate(-36.4678,-258.39)">
-<path style="opacity:1;fill-opacity:1;fill:#15a5ea;" d="m38.96,260.4,0,0.6213,8.098,0,0-0.6213z"/>
-<path style="opacity:1;fill-opacity:1;fill:#15a5ea;" d="m38.96,268.4,8.098,0,0-0.6213-8.098,0z"/>
-<path style="opacity:1;fill-opacity:1;fill:#15a5ea;" d="m38.96,262.3,0,0.6213,1.869,0,0-0.6213zm6.229,0,0,0.6213,1.869,0,0-0.6213z"/>
-<path style="opacity:1;fill-opacity:1;fill:#15a5ea;" d="m38.96,266,0,0.6213,1.869,0,0-0.6213zm6.229,0,0,0.6213,1.869,0,0-0.6213z"/>
-<path style="opacity:1;fill-opacity:1;fill:#15a5ea;" d="m38.96,264.8,8.098,0,0-0.6213-8.098,0z"/>
-<path style="opacity:1;fill:#15a5ea;" d="m40.83,263.9,0,1.243,4.36,0,0-1.243z"/>
-<path style="opacity:1;fill:#3366bb;" d="m40.47,260.4,0,8,5,0,0-8zm1,1,3,0,0,6-3,0z"/>
-<path style="opacity:1;fill:#3366bb;" d="m38.47,259.4,1,0,0,10-1,0z"/>
-<path style="opacity:1;fill:#3366bb;" d="m46.47,259.4,1,0,0,10-1,0z"/>
-</g>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13">
+       <path fill="#15a5ea" d="M2.492 2.01v.621h8.098V2.01zm0 8h8.098v-.621H2.492zm0-6.1v.621h1.87V3.91zm6.23 0v.621h1.868V3.91zm-6.23 3.7v.621h1.87V7.61zm6.23 0v.621h1.868V7.61zm-6.23-1.2h8.098v-.621H2.492z"/>
+       <path fill="#15a5ea" d="M4.362 5.51v1.243h4.36V5.51z"/>
+       <path fill="#36b" d="M4.002 2.01v8h5v-8zm1 1h3v6h-3zm-3-2h1v10h-1zm8 0h1v10h-1z"/>
 </svg>
index 19eac98..68c7ddc 100644 (file)
                        // OO.ui.ButtonWidget doesn't take focus itself (T128054)
                        $focus = $( '#mw-apisandbox-ui' ).find( document.activeElement );
                        if ( $focus.length ) {
-                               // eslint-disable-next-line jquery/no-event-shorthand
                                $focus[ 0 ].blur();
                        }
 
                                }
 
                                that.deprecatedItemsFieldset = new OO.ui.FieldsetLayout().addItems( deprecatedItems ).toggle( false );
-                               // eslint-disable-next-line jquery/no-animate-toggle
                                tmp = $( '<fieldset>' )
                                        .toggle( !that.deprecatedItemsFieldset.isEmpty() )
                                        .append(
index b777c88..58409b4 100644 (file)
@@ -16,6 +16,7 @@
                var blockTargetWidget = infuseIfExists( $( '#mw-bi-target' ) ),
                        anonOnlyField = infuseIfExists( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
                        enableAutoblockField = infuseIfExists( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
+                       hideUserWidget = infuseIfExists( $( '#mw-input-wpHideUser' ) ),
                        hideUserField = infuseIfExists( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
                        watchUserField = infuseIfExists( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
                        expiryWidget = infuseIfExists( $( '#mw-input-wpExpiry' ) ),
                                // infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
                                infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
                                isIndefinite = infinityValues.indexOf( expiryValue ) !== -1,
-                               editingRestrictionValue = editingRestrictionWidget ? editingRestrictionWidget.getValue() : undefined,
-                               editingIsSelected = editingWidget ? editingWidget.isSelected() : false;
+                               // editingRestrictionWidget only exists if partial blocks is enabled; if not, block must be sitewide
+                               editingRestrictionValue = editingRestrictionWidget ? editingRestrictionWidget.getValue() : 'sitewide',
+                               editingIsSelected = editingWidget ? editingWidget.isSelected() : false,
+                               isSitewide = editingIsSelected && editingRestrictionValue === 'sitewide';
 
                        if ( enableAutoblockField ) {
                                enableAutoblockField.toggle( !isNonEmptyIp );
                        }
                        if ( hideUserField ) {
-                               hideUserField.toggle( !isNonEmptyIp && isIndefinite );
+                               hideUserField.toggle( !isNonEmptyIp && isIndefinite && isSitewide );
+                               if ( !hideUserField.isVisible() ) {
+                                       hideUserWidget.setSelected( false );
+                               }
                        }
                        if ( anonOnlyField ) {
                                anonOnlyField.toggle( isIp || isEmpty );
                                editingRestrictionWidget.setDisabled( !editingIsSelected );
                        }
                        if ( pageRestrictionsWidget ) {
-                               pageRestrictionsWidget.setDisabled( !editingIsSelected || editingRestrictionValue === 'sitewide' );
+                               pageRestrictionsWidget.setDisabled( !editingIsSelected || isSitewide );
                        }
                        if ( namespaceRestrictionsWidget ) {
-                               namespaceRestrictionsWidget.setDisabled( !editingIsSelected || editingRestrictionValue === 'sitewide' );
+                               namespaceRestrictionsWidget.setDisabled( !editingIsSelected || isSitewide );
                        }
                        if ( preventTalkPageEdit && namespaceRestrictionsWidget ) {
                                // This option is disabled for partial blocks unless a namespace restriction
index 8abb8f2..77ca848 100644 (file)
                                };
                                img.src = dataURL;
                        }, mw.config.get( 'wgFileCanRotate' ) ? function ( data ) {
-                               var jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' );
+                               var jpegmeta = require( 'mediawiki.libs.jpegmeta' );
                                try {
                                        meta = jpegmeta( data, file.fileName );
                                        // eslint-disable-next-line no-underscore-dangle, camelcase
index 63d9623..a4f5d1a 100644 (file)
@@ -12,7 +12,6 @@
 
        // Dynamically show/hide the "other time" input under each dropdown
        $( '.mw-userrights-nested select' ).on( 'change', function ( e ) {
-               // eslint-disable-next-line jquery/no-animate-toggle
                $( e.target.parentNode ).find( 'input' ).toggle( $( e.target ).val() === 'other' );
        } );
 
index b7175d0..ac7a225 100644 (file)
                box-shadow: none;
        }
 
-       &:disabled,
-       &.mw-ui-quiet.mw-ui-progressive,
-       &.mw-ui-quiet.mw-ui-destructive {
+       &:disabled {
                background-color: @colorGray12;
                color: @colorBaseInverted;
                border-color: @colorGray12;
        // </div>
        //
        // Styleguide 2.1.1.
-       &.mw-ui-quiet {
+       &.mw-ui-quiet,
+       &.mw-ui-quiet.mw-ui-progressive,
+       &.mw-ui-quiet.mw-ui-destructive {
                background-color: transparent;
                // Quiet buttons all start gray, and reveal
                // progressive/destructive color on hover and active.
                        box-shadow: none;
                }
 
-               &:disabled {
+               &:disabled,
+               &:disabled:hover,
+               &:disabled:active {
                        background-color: transparent;
                        color: @colorDisabledText;
                        border-color: transparent;
index e41ed58..d9b1227 100644 (file)
                }
        } );
 
-       /**
-        * @method stickyRandomId
-        * @deprecated since 1.32 use getPageviewToken instead
-        */
-       mw.log.deprecate( mw.user, 'stickyRandomId', mw.user.getPageviewToken, 'Please use getPageviewToken instead' );
-
 }() );
index 65fe3d3..7cda45f 100644 (file)
@@ -1,7 +1,9 @@
 ( function () {
        'use strict';
 
-       var util;
+       var util,
+               config = require( './config.json' ),
+               origConfig = config;
 
        /**
         * Encode the string like PHP's rawurlencode
 
                /* Main body */
 
+               setOptionsForTest: function ( opts ) {
+                       if ( !window.QUnit ) {
+                               throw new Error( 'Modifying options not allowed outside unit tests' );
+                       }
+                       config = $.extend( {}, config, opts );
+               },
+
+               resetOptionsForTest: function () {
+                       if ( !window.QUnit ) {
+                               throw new Error( 'Resetting options not allowed outside unit tests' );
+                       }
+                       config = origConfig;
+               },
+
                /**
                 * Encode the string like PHP's rawurlencode
                 *
@@ -68,7 +84,7 @@
                 * @return {string} Encoded string
                 */
                escapeIdForAttribute: function ( str ) {
-                       var mode = mw.config.get( 'wgFragmentMode' )[ 0 ];
+                       var mode = config.FragmentMode[ 0 ];
 
                        return escapeIdInternal( str, mode );
                },
@@ -83,7 +99,7 @@
                 * @return {string} Encoded string
                 */
                escapeIdForLink: function ( str ) {
-                       var mode = mw.config.get( 'wgFragmentMode' )[ 0 ];
+                       var mode = config.FragmentMode[ 0 ];
 
                        return escapeIdInternal( str, mode );
                },
index 45bc2dd..0786048 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M 5,3 C 5,3 5,16.507875 5,17.25 5,19.208049 6.582269,21 8.502269,21 8.7138904,21 19,21 19,21 L 19,3 z m 8.002269,3 4,0 0,4 L 15.721019,8.71875 12.439769,12 15.721019,15.28125 17.002269,14 l 0,4 -4,0 1.3125,-1.3125 -3.71875,-3.6875 -3.59375,0 0,-2 3.59375,0 3.6875,-3.71875 z"/>
+       <path d="M5 3v14.25C5 19.208 6.582 21 8.502 21H19V3zm8.002 3h4v4l-1.281-1.281L12.44 12l3.281 3.281L17.002 14v4h-4l1.313-1.313L10.596 13H7.002v-2h3.594l3.688-3.719z"/>
 </svg>
index 7c83bdb..753c9d5 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="m 19,3 c 0,0 0,13.507875 0,14.25 C 19,19.208049 17.417731,21 15.497731,21 15.28611,21 5,21 5,21 L 5,3 z m -8.002269,3 -4,0 0,4 L 8.278981,8.71875 11.560231,12 8.278981,15.28125 6.997731,14 l 0,4 4,0 -1.3125,-1.3125 3.71875,-3.6875 3.59375,0 0,-2 -3.59375,0 -3.6875,-3.71875 z"/>
+       <path d="M19 3v14.25c0 1.958-1.582 3.75-3.502 3.75H5V3zm-8.002 3h-4v4l1.281-1.281L11.56 12l-3.28 3.281L6.998 14v4h4l-1.313-1.313L13.404 13h3.594v-2h-3.594L9.716 7.281z"/>
 </svg>
index fac87c9..011a171 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="m 12,12 5,0 0,-7 -5,0 z m -5,3 0,1 10,0 0,-1 m 0,-1 0,-1 -10,0 0,1 m 0,4 10,0 0,-1 -10,0 z M 11,7 7,7 7,8 11,8 z m 0,3 0,-1 -4,0 0,1 m 0,1 0,1 4,0 0,-1 M 11,5 7,5 7,6 11,6 z M 5,3 19,3 19,21 8.692,21 C 6.602,21 5,19.373 5,17.25 z"/>
+       <path d="M12 12h5V5h-5zm-5 3v1h10v-1m0-1v-1H7v1m0 4h10v-1H7zm4-11H7v1h4zm0 3V9H7v1m0 1v1h4v-1m0-6H7v1h4zM5 3h14v18H8.692C6.602 21 5 19.373 5 17.25z"/>
 </svg>
index 89dfa0d..db4ad43 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="m 12,12 -5,0 0,-7 5,0 z m 5,3 0,1 -10,0 0,-1 m 0,-1 0,-1 10,0 0,1 m 0,4 -10,0 0,-1 10,0 z m -4,-11 4,0 0,1 -4,0 z m 0,3 0,-1 4,0 0,1 m 0,1 0,1 -4,0 0,-1 m 0,-6 4,0 0,1 -4,0 z M 19,3 5,3 5,21 15.308,21 C 17.398,21 19,19.373 19,17.25 z"/>
+       <path d="M12 12H7V5h5zm5 3v1H7v-1m0-1v-1h10v1m0 4H7v-1h10zM13 7h4v1h-4zm0 3V9h4v1m0 1v1h-4v-1m0-6h4v1h-4zm6-2H5v18h10.308C17.398 21 19 19.373 19 17.25z"/>
 </svg>
index cf37b1c..d8c68a9 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-       <path d="M5 3v18h10c2 0 4-2 4-4V3zm7.644 13.572h-1.687v-1.6h1.687zm1.982-6c-.063.217-.148.404-.25.563-.104.16-.225.3-.36.423l-.402.364-.438.396c-.134.127-.25.273-.353.428-.103.16-.18.346-.233.555-.054.215-.08.474-.08.784h-1.36c0-.378.017-.696.057-.955.036-.26.098-.488.183-.688.085-.196.188-.37.31-.52.12-.15.267-.295.433-.44l.385-.332c.12-.105.233-.214.327-.34.098-.124.17-.265.228-.42.054-.155.08-.337.08-.55 0-.256-.044-.48-.133-.66-.09-.183-.197-.333-.322-.442-.125-.11-.26-.19-.403-.246-.143-.05-.268-.077-.376-.077-.52 0-.905.173-1.15.52-.247.345-.372.81-.372 1.39H8.962c0-.468.067-.895.206-1.282.14-.382.34-.714.604-.987.264-.273.582-.487.957-.632.37-.15.79-.223 1.252-.223.385 0 .743.06 1.078.174.33.114.622.282.868.5.246.218.443.487.586.814.143.323.215.692.215 1.1-.01.306-.04.565-.104.784z"/>
+       <path d="M5 3v18h10c2 0 4-2 4-4V3zm7.644 13.572h-1.687v-1.6h1.687zm1.982-6a2.144 2.144 0 0 1-.25.563c-.104.16-.225.3-.36.423l-.402.364-.438.396c-.134.127-.25.273-.353.428-.103.16-.18.346-.233.555-.054.215-.08.474-.08.784h-1.36c0-.378.017-.696.057-.955.036-.26.098-.488.183-.688.085-.196.188-.37.31-.52.12-.15.267-.295.433-.44l.385-.332c.12-.105.233-.214.327-.34.098-.124.17-.265.228-.42a1.67 1.67 0 0 0 .08-.55c0-.256-.044-.48-.133-.66a1.397 1.397 0 0 0-.322-.442 1.35 1.35 0 0 0-.403-.246 1.17 1.17 0 0 0-.376-.077c-.52 0-.905.173-1.15.52-.247.345-.372.81-.372 1.39H8.962c0-.468.067-.895.206-1.282a2.641 2.641 0 0 1 1.561-1.619c.37-.15.79-.223 1.252-.223.385 0 .743.06 1.078.174.33.114.622.282.868.5.246.218.443.487.586.814.143.323.215.692.215 1.1-.01.306-.04.565-.104.784z"/>
 </svg>
index fd6fdb5..bea394a 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-       <path d="m 5,3 0,14 c 0,2.125 1.911,4 4,4 l 10,0 0,-18 z m 7.644,13.572 -1.687,0 0,-1.601 1.687,0 z m 1.982,-6.001 c -0.063,0.218 -0.148,0.405 -0.251,0.564 -0.103,0.159 -0.224,0.3 -0.358,0.423 l -0.403,0.364 -0.438,0.396 c -0.134,0.127 -0.251,0.273 -0.353,0.428 -0.103,0.159 -0.179,0.346 -0.233,0.555 -0.054,0.214 -0.081,0.473 -0.081,0.783 l -1.36,0 c 0,-0.378 0.018,-0.696 0.058,-0.955 0.036,-0.259 0.098,-0.487 0.183,-0.687 0.085,-0.196 0.188,-0.369 0.309,-0.519 0.121,-0.15 0.268,-0.296 0.434,-0.441 l 0.385,-0.332 c 0.121,-0.105 0.233,-0.214 0.327,-0.341 0.098,-0.123 0.17,-0.264 0.228,-0.419 0.054,-0.155 0.081,-0.337 0.081,-0.551 0,-0.255 -0.045,-0.478 -0.134,-0.66 C 12.931,8.997 12.823,8.847 12.698,8.738 12.573,8.629 12.438,8.547 12.295,8.492 12.152,8.442 12.027,8.415 11.919,8.415 c -0.519,0 -0.904,0.173 -1.15,0.519 -0.246,0.346 -0.371,0.81 -0.371,1.392 l -1.436,0 C 8.962,9.857 9.029,9.43 9.168,9.043 9.307,8.661 9.508,8.329 9.772,8.056 10.036,7.783 10.354,7.569 10.729,7.424 11.1,7.274 11.521,7.201 11.982,7.201 c 0.385,0 0.743,0.059 1.078,0.173 0.331,0.114 0.622,0.282 0.868,0.5 0.246,0.218 0.443,0.487 0.586,0.814 0.143,0.323 0.215,0.692 0.215,1.101 -0.009,0.305 -0.04,0.564 -0.103,0.783 z"/>
+       <path d="M5 3v14c0 2.125 1.911 4 4 4h10V3zm7.644 13.572h-1.687v-1.601h1.687zm1.982-6.001a2.106 2.106 0 0 1-.609.987l-.403.364-.438.396a2.422 2.422 0 0 0-.353.428 1.881 1.881 0 0 0-.233.555 3.236 3.236 0 0 0-.081.783h-1.36c0-.378.018-.696.058-.955a2.7 2.7 0 0 1 .183-.687c.085-.196.188-.369.309-.519a3.59 3.59 0 0 1 .434-.441l.385-.332a2.15 2.15 0 0 0 .327-.341c.098-.123.17-.264.228-.419.054-.155.081-.337.081-.551a1.5 1.5 0 0 0-.134-.66 1.388 1.388 0 0 0-.322-.441 1.35 1.35 0 0 0-.403-.246 1.17 1.17 0 0 0-.376-.077c-.519 0-.904.173-1.15.519-.246.346-.371.81-.371 1.392H8.962c0-.469.067-.896.206-1.283a2.641 2.641 0 0 1 1.561-1.619 3.33 3.33 0 0 1 1.253-.223c.385 0 .743.059 1.078.173.331.114.622.282.868.5.246.218.443.487.586.814a2.7 2.7 0 0 1 .215 1.101c-.009.305-.04.564-.103.783z"/>
 </svg>
index 0df7397..bb6f316 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-       <path d="m 19,3 0,14 c 0,2.125 -1.911,4 -4,4 L 5,21 5,3 z m -7.644,13.572 1.687,0 0,-1.601 -1.687,0 z M 9.374,10.571 c 0.063,0.218 0.148,0.405 0.251,0.564 0.103,0.159 0.224,0.3 0.358,0.423 l 0.403,0.364 0.438,0.396 c 0.134,0.127 0.251,0.273 0.353,0.428 0.103,0.159 0.179,0.346 0.233,0.555 0.054,0.214 0.081,0.473 0.081,0.783 l 1.36,0 c 0,-0.378 -0.018,-0.696 -0.058,-0.955 C 12.757,12.87 12.695,12.642 12.61,12.442 12.525,12.246 12.422,12.073 12.301,11.923 12.18,11.773 12.033,11.627 11.867,11.482 L 11.482,11.15 c -0.121,-0.105 -0.233,-0.214 -0.327,-0.341 -0.098,-0.123 -0.17,-0.264 -0.228,-0.419 -0.054,-0.155 -0.081,-0.337 -0.081,-0.551 0,-0.255 0.045,-0.478 0.134,-0.66 0.089,-0.182 0.197,-0.332 0.322,-0.441 0.125,-0.109 0.26,-0.191 0.403,-0.246 0.143,-0.05 0.268,-0.077 0.376,-0.077 0.519,0 0.904,0.173 1.15,0.519 0.246,0.346 0.371,0.81 0.371,1.392 l 1.436,0 C 15.038,9.857 14.971,9.43 14.832,9.043 14.693,8.661 14.492,8.329 14.228,8.056 13.964,7.783 13.646,7.569 13.271,7.424 12.9,7.274 12.479,7.201 12.018,7.201 11.633,7.201 11.275,7.26 10.94,7.374 10.609,7.488 10.318,7.656 10.072,7.874 9.826,8.092 9.629,8.361 9.486,8.688 9.343,9.011 9.271,9.38 9.271,9.789 c 0.009,0.305 0.04,0.564 0.103,0.783 z"/>
+       <path d="M19 3v14c0 2.125-1.911 4-4 4H5V3zm-7.644 13.572h1.687v-1.601h-1.687zm-1.982-6.001a2.106 2.106 0 0 0 .609.987l.403.364.438.396c.134.127.251.273.353.428.103.159.179.346.233.555.054.214.081.473.081.783h1.36c0-.378-.018-.696-.058-.955a2.7 2.7 0 0 0-.183-.687 2.242 2.242 0 0 0-.309-.519 3.59 3.59 0 0 0-.434-.441l-.385-.332a2.15 2.15 0 0 1-.327-.341 1.513 1.513 0 0 1-.228-.419 1.671 1.671 0 0 1-.081-.551 1.5 1.5 0 0 1 .134-.66c.089-.182.197-.332.322-.441a1.35 1.35 0 0 1 .403-.246 1.17 1.17 0 0 1 .376-.077c.519 0 .904.173 1.15.519.246.346.371.81.371 1.392h1.436a3.77 3.77 0 0 0-.206-1.283 2.641 2.641 0 0 0-1.561-1.619 3.33 3.33 0 0 0-1.253-.223c-.385 0-.743.059-1.078.173a2.548 2.548 0 0 0-.868.5 2.304 2.304 0 0 0-.586.814 2.7 2.7 0 0 0-.215 1.101c.009.305.04.564.103.783z"/>
 </svg>
index fa6d213..f296ac5 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-       <path d="m 5,3 0,14 c 0,2.552 1.516,4 4,4 L 19,21 19,3 5,3 z m 9.375,3.78125 c 1.383569,0 2.655397,1.2079253 2.78125,2.625 0,0.83766 -0.373613,1.545585 -0.9375,2.125 l -1.65625,1.6875 c -0.438035,0.517306 -1.120698,0.8125 -1.875,0.8125 -1.132679,0 -1.902838,-0.689468 -2.40625,-1.65625 l 0.8125,-0.84375 c 0.312181,0.708538 0.776094,1.28125 1.65625,1.28125 0.37756,0 0.873293,-0.178438 1.125,-0.4375 l 1.65625,-1.6875 c 0.629266,-0.646428 0.629266,-1.666072 0,-2.3125 C 15.219068,8.1167557 14.75256,7.90625 14.375,7.90625 13.619881,7.90625 13.127595,8.4826941 12.625,9 12.312818,8.8708781 11.999682,8.84375 11.6875,8.84375 c -0.186328,0 -0.374147,0.03125 -0.5,0.03125 0.941448,-0.9046723 1.868493,-2.09375 3.1875,-2.09375 z M 11.09375,9.5625 c 1.132679,0 1.902837,0.720724 2.40625,1.6875 l -0.8125,0.8125 c -0.312182,-0.646428 -0.744844,-1.28125 -1.625,-1.28125 -0.37756,0 -0.873293,0.209689 -1.125,0.46875 l -1.65625,1.65625 c -0.6292663,0.579415 -0.6292663,1.666072 0,2.3125 0.3121814,0.258245 0.7474402,0.46875 1.125,0.46875 0.3775597,0 0.873293,-0.209688 1.125,-0.46875 l 0.5625,-0.59375 c 0.251706,0.12912 0.500318,0.15625 0.8125,0.15625 0.186328,0 0.376171,-3.9e-4 0.5625,-0.0625 L 11.3125,15.9375 c -0.941447,1.096721 -2.711924,1.033794 -3.71875,0 -1.0673009,-1.033795 -1.0673009,-2.774192 0,-3.875 l 1.625,-1.65625 C 9.6567847,9.8889443 10.339448,9.5625 11.09375,9.5625 z"/>
+       <path d="M5 3v14c0 2.552 1.516 4 4 4h10V3H5zm9.375 3.781c1.384 0 2.655 1.208 2.781 2.625 0 .838-.373 1.546-.937 2.125l-1.657 1.688c-.438.517-1.12.812-1.874.812-1.133 0-1.903-.69-2.407-1.656l.813-.844c.312.709.776 1.281 1.656 1.281.378 0 .873-.178 1.125-.437l1.656-1.688a1.65 1.65 0 0 0 0-2.312c-.312-.258-.778-.469-1.156-.469-.755 0-1.247.577-1.75 1.094-.312-.13-.625-.156-.938-.156-.186 0-.374.031-.5.031.942-.905 1.869-2.094 3.188-2.094zm-3.281 2.782c1.132 0 1.903.72 2.406 1.687l-.813.813c-.312-.647-.744-1.282-1.624-1.282-.378 0-.874.21-1.126.469l-1.656 1.656c-.629.58-.629 1.666 0 2.313.312.258.748.469 1.125.469.378 0 .874-.21 1.125-.47l.563-.593c.251.13.5.156.812.156.187 0 .376 0 .563-.062l-1.156 1.219c-.942 1.096-2.712 1.033-3.72 0-1.067-1.034-1.067-2.775 0-3.876l1.626-1.656a2.454 2.454 0 0 1 1.875-.844z"/>
 </svg>
index 48923c1..6c753d6 100644 (file)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-       <path d="m 19,3 0,14 c 0,2.552 -1.516,4 -4,4 L 5,21 5,3 19,3 z M 9.625,6.78125 c -1.383569,0 -2.655397,1.2079253 -2.78125,2.625 0,0.83766 0.373613,1.545585 0.9375,2.125 l 1.65625,1.6875 c 0.438035,0.517306 1.120698,0.8125 1.875,0.8125 1.132679,0 1.902838,-0.689468 2.40625,-1.65625 l -0.8125,-0.84375 c -0.312181,0.708538 -0.776094,1.28125 -1.65625,1.28125 -0.37756,0 -0.873293,-0.178438 -1.125,-0.4375 L 8.46875,10.6875 C 7.839484,10.041072 7.839484,9.021428 8.46875,8.375 8.780932,8.1167557 9.24744,7.90625 9.625,7.90625 c 0.755119,0 1.247405,0.5764441 1.75,1.09375 0.312182,-0.1291219 0.625318,-0.15625 0.9375,-0.15625 0.186328,0 0.374147,0.03125 0.5,0.03125 C 11.871052,7.9703277 10.944007,6.78125 9.625,6.78125 z m 3.28125,2.78125 c -1.132679,0 -1.902837,0.720724 -2.40625,1.6875 l 0.8125,0.8125 c 0.312182,-0.646428 0.744844,-1.28125 1.625,-1.28125 0.37756,0 0.873293,0.209689 1.125,0.46875 l 1.65625,1.65625 c 0.629266,0.579415 0.629266,1.666072 0,2.3125 -0.312181,0.258245 -0.74744,0.46875 -1.125,0.46875 -0.37756,0 -0.873293,-0.209688 -1.125,-0.46875 L 12.90625,14.625 c -0.251706,0.12912 -0.500318,0.15625 -0.8125,0.15625 -0.186328,0 -0.376171,-3.9e-4 -0.5625,-0.0625 l 1.15625,1.21875 c 0.941447,1.096721 2.711924,1.033794 3.71875,0 1.067301,-1.033795 1.067301,-2.774192 0,-3.875 l -1.625,-1.65625 C 14.343215,9.8889443 13.660552,9.5625 12.90625,9.5625 z"/>
+       <path d="M19 3v14c0 2.552-1.516 4-4 4H5V3h14zM9.625 6.781c-1.384 0-2.655 1.208-2.781 2.625 0 .838.373 1.546.937 2.125l1.657 1.688c.438.517 1.12.812 1.874.812 1.133 0 1.903-.69 2.407-1.656l-.813-.844c-.312.709-.776 1.281-1.656 1.281-.378 0-.873-.178-1.125-.437l-1.656-1.688a1.652 1.652 0 0 1 0-2.312c.312-.258.778-.469 1.156-.469.755 0 1.247.577 1.75 1.094.312-.13.625-.156.938-.156.186 0 .374.031.5.031-.942-.905-1.869-2.094-3.188-2.094zm3.281 2.782c-1.132 0-1.903.72-2.406 1.687l.813.813c.312-.647.744-1.282 1.624-1.282.378 0 .874.21 1.126.469l1.656 1.656c.629.58.629 1.666 0 2.313-.312.258-.748.469-1.125.469-.378 0-.874-.21-1.125-.47l-.563-.593c-.251.13-.5.156-.812.156-.187 0-.376 0-.563-.062l1.156 1.219c.942 1.096 2.712 1.033 3.72 0 1.067-1.034 1.067-2.775 0-3.876l-1.626-1.656a2.454 2.454 0 0 0-1.875-.844z"/>
 </svg>
index 03f02b4..b5ba6a6 100644 (file)
@@ -13,7 +13,7 @@
        'use strict';
 
        var mw, StringSet, log,
-               trackQueue = [];
+               hasOwn = Object.prototype.hasOwnProperty;
 
        /**
         * FNV132 hash function
         * @return {string} hash as an seven-character base 36 string
         */
        function fnv132( str ) {
-               /* eslint-disable no-bitwise */
                var hash = 0x811C9DC5,
                        i;
 
+               /* eslint-disable no-bitwise */
                for ( i = 0; i < str.length; i++ ) {
                        hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
                        hash ^= str.charCodeAt( i );
@@ -41,9 +41,9 @@
                while ( hash.length < 7 ) {
                        hash = '0' + hash;
                }
+               /* eslint-enable no-bitwise */
 
                return hash;
-               /* eslint-enable no-bitwise */
        }
 
        function defineFallbacks() {
        function logError( topic, data ) {
                var msg,
                        e = data.exception,
-                       source = data.source,
-                       module = data.module,
                        console = window.console;
 
                if ( console && console.log ) {
-                       msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
-                       if ( module ) {
-                               msg += ' in module ' + module;
-                       }
-                       msg += ( e ? ':' : '.' );
+                       msg = ( e ? 'Exception' : 'Error' ) +
+                               ' in ' + data.source +
+                               ( data.module ? ' in module ' + data.module : '' ) +
+                               ( e ? ':' : '.' );
+
                        console.log( msg );
 
                        // If we have an exception object, log it to the warning channel to trigger
                        this.set = function ( selection, value ) {
                                var s;
                                if ( arguments.length > 1 ) {
-                                       if ( typeof selection !== 'string' ) {
-                                               return false;
+                                       if ( typeof selection === 'string' ) {
+                                               setGlobalMapValue( this, selection, value );
+                                               return true;
                                        }
-                                       setGlobalMapValue( this, selection, value );
-                                       return true;
-                               }
-                               if ( typeof selection === 'object' ) {
+                               } else if ( typeof selection === 'object' ) {
                                        for ( s in selection ) {
                                                setGlobalMapValue( this, s, selection[ s ] );
                                        }
                        var s;
                        // Use `arguments.length` because `undefined` is also a valid value.
                        if ( arguments.length > 1 ) {
-                               if ( typeof selection !== 'string' ) {
-                                       return false;
+                               // Set one key
+                               if ( typeof selection === 'string' ) {
+                                       this.values[ selection ] = value;
+                                       return true;
                                }
-                               this.values[ selection ] = value;
-                               return true;
-                       }
-                       if ( typeof selection === 'object' ) {
+                       } else if ( typeof selection === 'object' ) {
+                               // Set multiple keys
                                for ( s in selection ) {
                                        this.values[ s ] = selection[ s ];
                                }
                 * @param {string} key Name of property to create in `obj`
                 * @param {Mixed} val The value this property should return when accessed
                 * @param {string} [msg] Optional text to include in the deprecation message
-                * @param {string} [logName=key] Optional custom name for the feature.
-                *  This is used instead of `key` in the message and `mw.deprecate` tracking.
+                * @param {string} [logName] Name for the feature for logging and tracking
+                *  purposes. Except for properties of the window object, tracking is only
+                *  enabled if logName is set.
                 */
                log.deprecate = function ( obj, key, val, msg, logName ) {
                        var stacks;
                        function maybeLog() {
-                               var name,
+                               var name = logName || key,
                                        trace = new Error().stack;
                                if ( !stacks ) {
                                        stacks = new StringSet();
                                }
                                if ( !stacks.has( trace ) ) {
                                        stacks.add( trace );
-                                       name = logName || key;
-                                       mw.track( 'mw.deprecate', name );
+                                       if ( logName || obj === window ) {
+                                               mw.track( 'mw.deprecate', name );
+                                       }
                                        mw.log.warn(
-                                               'Use of "' + name + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' )
+                                               'Use of "' + name + '" is deprecated.' + ( msg ? ' ' + msg : '' )
                                        );
                                }
                        }
        mw = {
                redefineFallbacksForTest: function () {
                        if ( !window.QUnit ) {
-                               throw new Error( 'Reset not allowed outside unit tests' );
+                               throw new Error( 'Not allowed' );
                        }
                        defineFallbacks();
                },
                 * @return {number} Current time
                 */
                now: function () {
-                       // Optimisation: Define the shortcut on first call, not at module definition.
+                       // Optimisation: Make startup initialisation faster by defining the
+                       // shortcut on first call, not at module definition.
                        var perf = window.performance,
                                navStart = perf && perf.timing && perf.timing.navigationStart;
 
                        // Define the relevant shortcut
-                       mw.now = navStart && typeof perf.now === 'function' ?
+                       mw.now = navStart && perf.now ?
                                function () { return navStart + perf.now(); } :
                                Date.now;
 
                /**
                 * List of all analytic events emitted so far.
                 *
+                * Exposed only for use by mediawiki.base.
+                *
                 * @private
                 * @property {Array}
                 */
-               trackQueue: trackQueue,
+               trackQueue: [],
 
                track: function ( topic, data ) {
-                       trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
-                       // The base module extends this method to fire events here
+                       mw.trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
+                       // This method is extended by mediawiki.base to also fire events.
                },
 
                /**
                                                        i -= 1;
                                                        try {
                                                                if ( failed && job.error ) {
-                                                                       job.error( new Error( 'Module has failed dependencies' ), job.dependencies );
+                                                                       job.error( new Error( 'Failed dependencies' ), job.dependencies );
                                                                } else if ( !failed && job.ready ) {
                                                                        job.ready();
                                                                }
 
                                // Add base modules
                                if ( baseModules.indexOf( module ) === -1 ) {
-                                       baseModules.forEach( function ( baseModule ) {
-                                               if ( resolved.indexOf( baseModule ) === -1 ) {
-                                                       resolved.push( baseModule );
+                                       for ( i = 0; i < baseModules.length; i++ ) {
+                                               if ( resolved.indexOf( baseModules[ i ] ) === -1 ) {
+                                                       resolved.push( baseModules[ i ] );
                                                }
-                                       } );
+                                       }
                                }
 
                                // Tracks down dependencies
                                return resolved;
                        }
 
+                       /**
+                        * Resolve a relative file path.
+                        *
+                        * For example, resolveRelativePath( '../foo.js', 'resources/src/bar/bar.js' )
+                        * returns 'resources/src/foo.js'.
+                        *
+                        * @param {string} relativePath Relative file path, starting with ./ or ../
+                        * @param {string} basePath Path of the file (not directory) relativePath is relative to
+                        * @return {string|null} Resolved path, or null if relativePath does not start with ./ or ../
+                        */
+                       function resolveRelativePath( relativePath, basePath ) {
+                               var prefixes, prefix, baseDirParts,
+                                       relParts = relativePath.match( /^((?:\.\.?\/)+)(.*)$/ );
+
+                               if ( !relParts ) {
+                                       return null;
+                               }
+
+                               baseDirParts = basePath.split( '/' );
+                               // basePath looks like 'foo/bar/baz.js', so baseDirParts looks like [ 'foo', 'bar, 'baz.js' ]
+                               // Remove the file component at the end, so that we are left with only the directory path
+                               baseDirParts.pop();
+
+                               prefixes = relParts[ 1 ].split( '/' );
+                               // relParts[ 1 ] looks like '../../', so prefixes looks like [ '..', '..', '' ]
+                               // Remove the empty element at the end
+                               prefixes.pop();
+
+                               // For every ../ in the path prefix, remove one directory level from baseDirParts
+                               while ( ( prefix = prefixes.pop() ) !== undefined ) {
+                                       if ( prefix === '..' ) {
+                                               baseDirParts.pop();
+                                       }
+                               }
+
+                               // If there's anything left of the base path, prepend it to the file path
+                               return ( baseDirParts.length ? baseDirParts.join( '/' ) + '/' : '' ) + relParts[ 2 ];
+                       }
+
+                       /**
+                        * Make a require() function scoped to a package file
+                        * @private
+                        * @param {Object} moduleObj Module object from the registry
+                        * @param {string} basePath Path of the file this is scoped to. Used for relative paths.
+                        * @return {Function}
+                        */
+                       function makeRequireFunction( moduleObj, basePath ) {
+                               return function require( moduleName ) {
+                                       var fileName, fileContent, result, moduleParam,
+                                               scriptFiles = moduleObj.script.files;
+                                       fileName = resolveRelativePath( moduleName, basePath );
+                                       if ( fileName === null ) {
+                                               // Not a relative path, so it's a module name
+                                               return mw.loader.require( moduleName );
+                                       }
+
+                                       if ( !hasOwn.call( scriptFiles, fileName ) ) {
+                                               throw new Error( 'Cannot require() undefined file ' + fileName );
+                                       }
+                                       if ( hasOwn.call( moduleObj.packageExports, fileName ) ) {
+                                               // File has already been executed, return the cached result
+                                               return moduleObj.packageExports[ fileName ];
+                                       }
+
+                                       fileContent = scriptFiles[ fileName ];
+                                       if ( typeof fileContent === 'function' ) {
+                                               moduleParam = { exports: {} };
+                                               fileContent( makeRequireFunction( moduleObj, fileName ), moduleParam );
+                                               result = moduleParam.exports;
+                                       } else {
+                                               // fileContent is raw data, just pass it through
+                                               result = fileContent;
+                                       }
+                                       moduleObj.packageExports[ fileName ] = result;
+                                       return result;
+                               };
+                       }
+
                        /**
                         * Load and execute a script.
                         *
                                                // these as the server will deny them anyway (T101806).
                                                if ( registry[ module ].group === 'private' ) {
                                                        setAndPropagate( module, 'error' );
-                                                       return;
+                                               } else {
+                                                       queue.push( module );
                                                }
-                                               queue.push( module );
                                        }
                                } );
 
                                $CODE.profileExecuteStart();
 
                                runScript = function () {
-                                       var script, markModuleReady, nestedAddScript;
+                                       var script, markModuleReady, nestedAddScript, mainScript;
 
                                        $CODE.profileScriptStart();
                                        script = registry[ module ].script;
                                        try {
                                                if ( Array.isArray( script ) ) {
                                                        nestedAddScript( script, markModuleReady, 0 );
-                                               } else if ( typeof script === 'function' ) {
-                                                       // Keep in sync with queueModuleScript() for debug mode
-                                                       if ( module === 'jquery' ) {
-                                                               // This is a special case for when 'jquery' itself is being loaded.
-                                                               // - The standard jquery.js distribution does not set `window.jQuery`
-                                                               //   in CommonJS-compatible environments (Node.js, AMD, RequireJS, etc.).
-                                                               // - MediaWiki's 'jquery' module also bundles jquery.migrate.js, which
-                                                               //   in a CommonJS-compatible environment, will use require('jquery'),
-                                                               //   but that can't work when we're still inside that module.
-                                                               script();
+                                               } else if (
+                                                       typeof script === 'function' || (
+                                                               typeof script === 'object' &&
+                                                               script !== null
+                                                       )
+                                               ) {
+                                                       if ( typeof script === 'function' ) {
+                                                               // Keep in sync with queueModuleScript() for debug mode
+                                                               if ( module === 'jquery' ) {
+                                                                       // This is a special case for when 'jquery' itself is being loaded.
+                                                                       // - The standard jquery.js distribution does not set `window.jQuery`
+                                                                       //   in CommonJS-compatible environments (Node.js, AMD, RequireJS, etc.).
+                                                                       // - MediaWiki's 'jquery' module also bundles jquery.migrate.js, which
+                                                                       //   in a CommonJS-compatible environment, will use require('jquery'),
+                                                                       //   but that can't work when we're still inside that module.
+                                                                       script();
+                                                               } else {
+                                                                       // Pass jQuery twice so that the signature of the closure which wraps
+                                                                       // the script can bind both '$' and 'jQuery'.
+                                                                       script( window.$, window.$, mw.loader.require, registry[ module ].module );
+                                                               }
                                                        } else {
-                                                               // Pass jQuery twice so that the signature of the closure which wraps
-                                                               // the script can bind both '$' and 'jQuery'.
-                                                               script( window.$, window.$, mw.loader.require, registry[ module ].module );
+                                                               mainScript = script.files[ script.main ];
+                                                               if ( typeof mainScript !== 'function' ) {
+                                                                       throw new Error( 'Main file ' + script.main + ' in module ' + module +
+                                                                               ' must be of type function, found ' + typeof mainScript );
+                                                               }
+                                                               // jQuery parameters are not passed for multi-file modules
+                                                               mainScript(
+                                                                       makeRequireFunction( registry[ module ], script.main ),
+                                                                       registry[ module ].module
+                                                               );
                                                        }
                                                        markModuleReady();
-
                                                } else if ( typeof script === 'string' ) {
                                                        // Site and user modules are legacy scripts that run in the global scope.
                                                        // This is transported as a string instead of a function to avoid needing
                                        module: {
                                                exports: {}
                                        },
+                                       // module.export objects for each package file inside this module
+                                       packageExports: {},
                                        version: String( version || '' ),
                                        dependencies: dependencies || [],
                                        group: typeof group === 'string' ? group : null,
                                 *  as '`[name]@[version]`". This version should match the requested version
                                 *  (from #batchRequest and #registry). This avoids race conditions (T117587).
                                 *  For back-compat with MediaWiki 1.27 and earlier, the version may be omitted.
-                                * @param {Function|Array|string} [script] Function with module code, list of URLs
-                                *  to load via `<script src>`, or string of module code for `$.globalEval()`.
+                                * @param {Function|Array|string|Object} [script] Module code. This can be a function,
+                                *  a list of URLs to load via `<script src>`, a string for `$.globalEval()`, or an
+                                *  object like {"files": {"foo.js":function, "bar.js": function, ...}, "main": "foo.js"}.
+                                *  If an object is provided, the main file will be executed immediately, and the other
+                                *  files will only be executed if loaded via require(). If a function or string is
+                                *  provided, it will be executed/evaluated immediately. If an array is provided, all
+                                *  URLs in the array will be loaded immediately, and executed as soon as they arrive.
                                 * @param {Object} [style] Should follow one of the following patterns:
                                 *
                                 *     { "css": [css, ..] }
                                                                return;
                                                        }
                                                        // Unknown type
-                                                       throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
+                                                       throw new Error( 'type must be text/css or text/javascript, found ' + type );
                                                }
                                                // Called with single module
                                                modules = [ modules ];
                                        // Only ready modules can be required
                                        if ( state !== 'ready' ) {
                                                // Module may've forgotten to declare a dependency
-                                               throw new Error( 'Module "' + moduleName + '" is not loaded.' );
+                                               throw new Error( 'Module "' + moduleName + '" is not loaded' );
                                        }
 
                                        return registry[ moduleName ].module.exports;
                                                        this.stats.hits++;
                                                        return this.items[ key ];
                                                }
+
                                                this.stats.misses++;
                                                return false;
                                        },
                                         */
                                        set: function ( module ) {
                                                var key, args, src,
+                                                       encodedScript,
                                                        descriptor = mw.loader.moduleRegistry[ module ];
 
                                                key = getModuleKey( module );
                                                }
 
                                                try {
+                                                       if ( typeof descriptor.script === 'function' ) {
+                                                               // Function literal: cast to string
+                                                               encodedScript = String( descriptor.script );
+                                                       } else if (
+                                                               // Plain object: serialise as object literal (not JSON),
+                                                               // making sure to preserve the functions.
+                                                               typeof descriptor.script === 'object' &&
+                                                               descriptor.script &&
+                                                               !Array.isArray( descriptor.script )
+                                                       ) {
+                                                               encodedScript = '{' +
+                                                                       'main:' + JSON.stringify( descriptor.script.main ) + ',' +
+                                                                       'files:{' +
+                                                                       Object.keys( descriptor.script.files ).map( function ( key ) {
+                                                                               var value = descriptor.script.files[ key ];
+                                                                               return JSON.stringify( key ) + ':' +
+                                                                                       ( typeof value === 'function' ? value : JSON.stringify( value ) );
+                                                                       } ).join( ',' ) +
+                                                                       '}}';
+                                                       } else {
+                                                               // Array of urls, or null.
+                                                               encodedScript = JSON.stringify( descriptor.script );
+                                                       }
                                                        args = [
                                                                JSON.stringify( key ),
-                                                               typeof descriptor.script === 'function' ?
-                                                                       String( descriptor.script ) :
-                                                                       JSON.stringify( descriptor.script ),
+                                                               encodedScript,
                                                                JSON.stringify( descriptor.style ),
                                                                JSON.stringify( descriptor.messages ),
                                                                JSON.stringify( descriptor.templates )
index b6c7b03..0e6a3ee 100644 (file)
@@ -1199,7 +1199,7 @@ class ParserTestRunner {
         * @return array
         */
        private function listTables() {
-               global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
+               global $wgActorTableSchemaMigrationStage;
 
                $tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
                        'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks',
@@ -1209,14 +1209,9 @@ class ParserTestRunner {
                        'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
                        'archive', 'user_groups', 'page_props', 'category',
                        'slots', 'content', 'slot_roles', 'content_models',
+                       'comment', 'revision_comment_temp',
                ];
 
-               if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
-                       // The new tables for comments are in use
-                       $tables[] = 'comment';
-                       $tables[] = 'revision_comment_temp';
-               }
-
                if ( $wgActorTableSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
                        // The new tables for actors are in use
                        $tables[] = 'actor';
index 5e1f1a9..1f1c395 100644 (file)
@@ -5,6 +5,7 @@
  *
  * @see https://github.com/sebastianbergmann/phpunit/blob/master/src/Extensions/PhptTestCase.php
  * @author Sam Smith <samsmith@wikimedia.org>
+ * @coversNothing
  */
 class LessFileCompilationTest extends ResourceLoaderTestCase {
 
index 672ab4a..79ce634 100644 (file)
@@ -29,9 +29,9 @@ trait PHPUnit4And6Compat {
         * is a temporary backwards-compatibility layer while we transition.
         */
        public function setExpectedException( $name, $message = '', $code = null ) {
-               if ( is_callable( [ $this, 'expectException' ] ) ) {
+               if ( is_callable( 'parent::expectException' ) ) {
                        if ( $name !== null ) {
-                               $this->expectException( $name );
+                               parent::expectException( $name );
                        }
                        if ( $message !== '' ) {
                                $this->expectExceptionMessage( $message );
@@ -44,6 +44,18 @@ trait PHPUnit4And6Compat {
                }
        }
 
+       /**
+        * Future-compatible layer for PHPUnit 4's setExpectedException.
+        */
+       public function expectException( $exception ) {
+               if ( is_callable( 'parent::expectException' ) ) {
+                       parent::expectException( $exception );
+                       return;
+               }
+
+               parent::setExpectedException( $exception );
+       }
+
        /**
         * @see PHPUnit_Framework_TestCase::getMock
         *
diff --git a/tests/phpunit/data/media/translated.svg b/tests/phpunit/data/media/translated.svg
new file mode 100644 (file)
index 0000000..afd9fb4
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<svg xmlns="http://www.w3.org/2000/svg">
+       <g>
+               <switch>
+                       <text systemLanguage="ru"><tspan>RU</tspan></text>
+                       <text systemLanguage="de"><tspan>DE</tspan></text>
+                       <text>fallback</text>
+               </switch>
+       </g>
+</svg>
diff --git a/tests/phpunit/data/resourceloader/sample.json b/tests/phpunit/data/resourceloader/sample.json
new file mode 100644 (file)
index 0000000..f2b69d0
--- /dev/null
@@ -0,0 +1,4 @@
+{
+       "foo": "bar",
+       "answer": 42
+}
index 8288cae..ec806ae 100644 (file)
@@ -6,6 +6,7 @@
  *
  * @group Dump
  * @group large
+ * @coversNothing
  */
 class ExportDemoTest extends DumpTestCase {
 
index b761d29..15c70bc 100644 (file)
@@ -571,10 +571,8 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
        public static function provideInsertRoundTrip() {
                $db = wfGetDB( DB_REPLICA ); // for timestamps
 
-               $ipbfields = [
-               ];
-               $revfields = [
-               ];
+               $comment = MediaWikiServices::getInstance()->getCommentStore()
+                       ->createComment( wfGetDB( DB_MASTER ), '' );
 
                return [
                        'recentchanges' => [ 'recentchanges', 'rc_user', 'rc_id', [
@@ -584,12 +582,14 @@ class ActorMigrationTest extends MediaWikiLangTestCase {
                                'rc_this_oldid' => 42,
                                'rc_last_oldid' => 41,
                                'rc_source' => 'test',
+                               'rc_comment_id' => $comment->id,
                        ] ],
                        'ipblocks' => [ 'ipblocks', 'ipb_by', 'ipb_id', [
                                'ipb_range_start' => '',
                                'ipb_range_end' => '',
                                'ipb_timestamp' => $db->timestamp(),
                                'ipb_expiry' => $db->getInfinity(),
+                               'ipb_reason_id' => $comment->id,
                        ] ],
                        'revision' => [ 'revision', 'rev_user', 'rev_id', [
                                'rev_page' => 42,
index e4dce12..a151080 100644 (file)
@@ -745,4 +745,12 @@ class BlockTest extends MediaWikiLangTestCase {
                $block->delete();
        }
 
+       /**
+        * @covers Block::prevents
+        */
+       public function testBlockAllowsPurge() {
+               $block = new Block();
+               $this->assertFalse( $block->prevents( 'purge' ) );
+       }
+
 }
index 4360343..78c5bf3 100644 (file)
@@ -18,6 +18,17 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                'comment',
        ];
 
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               return [
+                       'scripts' => [
+                               __DIR__ . '/CommentStoreTest.sql',
+                       ],
+                       'drop' => [],
+                       'create' => [ 'commentstore1', 'commentstore2', 'commentstore2_temp' ],
+                       'alter' => [],
+               ];
+       }
+
        /**
         * Create a store for a particular stage
         * @param int $stage
@@ -25,6 +36,16 @@ class CommentStoreTest extends MediaWikiLangTestCase {
         */
        protected function makeStore( $stage ) {
                $store = new CommentStore( MediaWikiServices::getInstance()->getContentLanguage(), $stage );
+
+               TestingAccessWrapper::newFromObject( $store )->tempTables += [ 'cs2_comment' => [
+                       'table' => 'commentstore2_temp',
+                       'pk' => 'cs2t_id',
+                       'field' => 'cs2t_comment_id',
+                       'joinPK' => 'cs2_id',
+                       'stage' => MIGRATION_OLD,
+                       'deprecatedIn' => null,
+               ] ];
+
                return $store;
        }
 
@@ -38,6 +59,16 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                $this->hideDeprecated( 'CommentStore::newKey' );
                $store = CommentStore::newKey( $key );
                TestingAccessWrapper::newFromObject( $store )->stage = $stage;
+
+               TestingAccessWrapper::newFromObject( $store )->tempTables += [ 'cs2_comment' => [
+                       'table' => 'commentstore2_temp',
+                       'pk' => 'cs2t_id',
+                       'field' => 'cs2t_comment_id',
+                       'joinPK' => 'cs2_id',
+                       'stage' => MIGRATION_OLD,
+                       'deprecatedIn' => null,
+               ] ];
+
                return $store;
        }
 
@@ -360,12 +391,13 @@ class CommentStoreTest extends MediaWikiLangTestCase {
         * @param string $table
         * @param string $key
         * @param string $pk
-        * @param string $extraFields
         * @param string|Message $comment
         * @param array|null $data
         * @param array $expect
         */
-       public function testInsertRoundTrip( $table, $key, $pk, $extraFields, $comment, $data, $expect ) {
+       public function testInsertRoundTrip( $table, $key, $pk, $comment, $data, $expect ) {
+               static $id = 1;
+
                $expectOld = [
                        'text' => $expect['text'],
                        'message' => new RawMessage( '$1', [ $expect['text'] ] ),
@@ -381,12 +413,8 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                ];
 
                foreach ( $stages as $writeStage => $possibleReadStages ) {
-                       if ( $key === 'ipb_reason' ) {
-                               $extraFields['ipb_address'] = __CLASS__ . "#$writeStage";
-                       }
-
                        $wstore = $this->makeStore( $writeStage );
-                       $usesTemp = $key === 'rev_comment';
+                       $usesTemp = $key === 'cs2_comment';
 
                        if ( $usesTemp ) {
                                list( $fields, $callback ) = $wstore->insertWithTempTable(
@@ -407,8 +435,7 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                                $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
                        }
 
-                       $this->db->insert( $table, $extraFields + $fields, __METHOD__ );
-                       $id = $this->db->insertId();
+                       $this->db->insert( $table, [ $pk => ++$id ] + $fields, __METHOD__ );
                        if ( $usesTemp ) {
                                $callback( $id );
                        }
@@ -452,14 +479,15 @@ class CommentStoreTest extends MediaWikiLangTestCase {
         * @param string $table
         * @param string $key
         * @param string $pk
-        * @param string $extraFields
         * @param string|Message $comment
         * @param array|null $data
         * @param array $expect
         */
        public function testInsertRoundTrip_withKeyConstruction(
-               $table, $key, $pk, $extraFields, $comment, $data, $expect
+               $table, $key, $pk, $comment, $data, $expect
        ) {
+               static $id = 1000;
+
                $expectOld = [
                        'text' => $expect['text'],
                        'message' => new RawMessage( '$1', [ $expect['text'] ] ),
@@ -475,12 +503,8 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                ];
 
                foreach ( $stages as $writeStage => $possibleReadStages ) {
-                       if ( $key === 'ipb_reason' ) {
-                               $extraFields['ipb_address'] = __CLASS__ . "#$writeStage";
-                       }
-
                        $wstore = $this->makeStoreWithKey( $writeStage, $key );
-                       $usesTemp = $key === 'rev_comment';
+                       $usesTemp = $key === 'cs2_comment';
 
                        if ( $usesTemp ) {
                                list( $fields, $callback ) = $wstore->insertWithTempTable(
@@ -501,8 +525,7 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                                $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
                        }
 
-                       $this->db->insert( $table, $extraFields + $fields, __METHOD__ );
-                       $id = $this->db->insertId();
+                       $this->db->insert( $table, [ $pk => ++$id ] + $fields, __METHOD__ );
                        if ( $usesTemp ) {
                                $callback( $id );
                        }
@@ -547,60 +570,48 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                $msgComment = new Message( 'parentheses', [ 'message comment' ] );
                $textCommentMsg = new RawMessage( '$1', [ 'text comment' ] );
                $nestedMsgComment = new Message( [ 'parentheses', 'rawmessage' ], [ new Message( 'mainpage' ) ] );
-               $ipbfields = [
-                       'ipb_range_start' => '',
-                       'ipb_range_end' => '',
-                       'ipb_timestamp' => $db->timestamp(),
-                       'ipb_expiry' => $db->getInfinity(),
-               ];
-               $revfields = [
-                       'rev_page' => 42,
-                       'rev_text_id' => 42,
-                       'rev_len' => 0,
-                       'rev_timestamp' => $db->timestamp(),
-               ];
                $comStoreComment = new CommentStoreComment(
                        null, 'comment store comment', null, [ 'foo' => 'bar' ]
                );
 
                return [
                        'Simple table, text comment' => [
-                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', null, [
+                               'commentstore1', 'cs1_comment', 'cs1_id', 'text comment', null, [
                                        'text' => 'text comment',
                                        'message' => $textCommentMsg,
                                        'data' => null,
                                ]
                        ],
                        'Simple table, text comment with data' => [
-                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, 'text comment', [ 'message' => 42 ], [
+                               'commentstore1', 'cs1_comment', 'cs1_id', 'text comment', [ 'message' => 42 ], [
                                        'text' => 'text comment',
                                        'message' => $textCommentMsg,
                                        'data' => [ 'message' => 42 ],
                                ]
                        ],
                        'Simple table, message comment' => [
-                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, null, [
+                               'commentstore1', 'cs1_comment', 'cs1_id', $msgComment, null, [
                                        'text' => '(message comment)',
                                        'message' => $msgComment,
                                        'data' => null,
                                ]
                        ],
                        'Simple table, message comment with data' => [
-                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $msgComment, [ 'message' => 42 ], [
+                               'commentstore1', 'cs1_comment', 'cs1_id', $msgComment, [ 'message' => 42 ], [
                                        'text' => '(message comment)',
                                        'message' => $msgComment,
                                        'data' => [ 'message' => 42 ],
                                ]
                        ],
                        'Simple table, nested message comment' => [
-                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, $nestedMsgComment, null, [
+                               'commentstore1', 'cs1_comment', 'cs1_id', $nestedMsgComment, null, [
                                        'text' => '(Main Page)',
                                        'message' => $nestedMsgComment,
                                        'data' => null,
                                ]
                        ],
                        'Simple table, CommentStoreComment' => [
-                               'ipblocks', 'ipb_reason', 'ipb_id', $ipbfields, clone $comStoreComment, [ 'baz' => 'baz' ], [
+                               'commentstore1', 'cs1_comment', 'cs1_id', clone $comStoreComment, [ 'baz' => 'baz' ], [
                                        'text' => 'comment store comment',
                                        'message' => $comStoreComment->message,
                                        'data' => [ 'foo' => 'bar' ],
@@ -608,42 +619,42 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                        ],
 
                        'Revision, text comment' => [
-                               'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', null, [
+                               'commentstore2', 'cs2_comment', 'cs2_id', 'text comment', null, [
                                        'text' => 'text comment',
                                        'message' => $textCommentMsg,
                                        'data' => null,
                                ]
                        ],
                        'Revision, text comment with data' => [
-                               'revision', 'rev_comment', 'rev_id', $revfields, 'text comment', [ 'message' => 42 ], [
+                               'commentstore2', 'cs2_comment', 'cs2_id', 'text comment', [ 'message' => 42 ], [
                                        'text' => 'text comment',
                                        'message' => $textCommentMsg,
                                        'data' => [ 'message' => 42 ],
                                ]
                        ],
                        'Revision, message comment' => [
-                               'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, null, [
+                               'commentstore2', 'cs2_comment', 'cs2_id', $msgComment, null, [
                                        'text' => '(message comment)',
                                        'message' => $msgComment,
                                        'data' => null,
                                ]
                        ],
                        'Revision, message comment with data' => [
-                               'revision', 'rev_comment', 'rev_id', $revfields, $msgComment, [ 'message' => 42 ], [
+                               'commentstore2', 'cs2_comment', 'cs2_id', $msgComment, [ 'message' => 42 ], [
                                        'text' => '(message comment)',
                                        'message' => $msgComment,
                                        'data' => [ 'message' => 42 ],
                                ]
                        ],
                        'Revision, nested message comment' => [
-                               'revision', 'rev_comment', 'rev_id', $revfields, $nestedMsgComment, null, [
+                               'commentstore2', 'cs2_comment', 'cs2_id', $nestedMsgComment, null, [
                                        'text' => '(Main Page)',
                                        'message' => $nestedMsgComment,
                                        'data' => null,
                                ]
                        ],
                        'Revision, CommentStoreComment' => [
-                               'revision', 'rev_comment', 'rev_id', $revfields, clone $comStoreComment, [ 'baz' => 'baz' ], [
+                               'commentstore2', 'cs2_comment', 'cs2_id', clone $comStoreComment, [ 'baz' => 'baz' ], [
                                        'text' => 'comment store comment',
                                        'message' => $comStoreComment->message,
                                        'data' => [ 'foo' => 'bar' ],
diff --git a/tests/phpunit/includes/CommentStoreTest.sql b/tests/phpunit/includes/CommentStoreTest.sql
new file mode 100644 (file)
index 0000000..f95781d
--- /dev/null
@@ -0,0 +1,17 @@
+-- These are carefully crafted to work in all five supported databases
+
+CREATE TABLE /*_*/commentstore1 (
+  cs1_id integer not null,
+  cs1_comment varchar(200),
+  cs1_comment_id integer
+);
+
+CREATE TABLE /*_*/commentstore2 (
+  cs2_id integer not null,
+  cs2_comment varchar(200)
+);
+
+CREATE TABLE /*_*/commentstore2_temp (
+  cs2t_id integer not null,
+  cs2t_comment_id integer
+);
index 55d8fbb..f5fef61 100644 (file)
@@ -368,6 +368,9 @@ class EditPageTest extends MediaWikiLangTestCase {
                }
        }
 
+       /**
+        * @covers EditPage
+        */
        public function testUpdatePage() {
                $checkIds = [];
 
@@ -414,6 +417,9 @@ class EditPageTest extends MediaWikiLangTestCase {
                $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
        }
 
+       /**
+        * @covers EditPage
+        */
        public function testUpdatePageTrx() {
                $text = "one";
                $edit = [
@@ -684,6 +690,7 @@ hello
 
        /**
         * @depends testAutoMerge
+        * @covers EditPage
         */
        public function testCheckDirectEditingDisallowed_forNonTextContent() {
                $title = Title::newFromText( 'Dummy:NonTextPageForEditPage' );
index 916a6eb..77bbc07 100644 (file)
@@ -187,6 +187,7 @@ class MediaWikiTest extends MediaWikiTestCase {
 
        /**
         * Test a post-send job can not set cookies (T191537).
+        * @coversNothing
         */
        public function testPostSendJobDoesNotSetCookie() {
                // Prevent updates from running
index d75c0e5..5d77ceb 100644 (file)
@@ -400,6 +400,9 @@ class MessageTest extends MediaWikiLangTestCase {
                $this->assertSame( 'example &amp;', $msg->escaped() );
        }
 
+       /**
+        * @covers CoreTagHooks::html
+        */
        public function testRawHtmlInMsg() {
                $this->setMwGlobals( 'wgRawHtml', true );
                // We have to reset the core hook registration.
index 607f4f7..1b2b159 100644 (file)
@@ -65,6 +65,7 @@ class MovePageTest extends MediaWikiTestCase {
 
        /**
         * Test for the move operation being aborted via the TitleMove hook
+        * @covers MovePage::move
         */
        public function testMoveAbortedByTitleMoveHook() {
                $error = 'Preventing move operation with TitleMove hook.';
index 00a08a7..7d40d8c 100644 (file)
@@ -912,7 +912,7 @@ class OutputPageTest extends MediaWikiTestCase {
         * @param array $args Array of form [ category name => sort key ]
         * @param array $fakeResults Array of form [ category name => value to return from mocked
         *   LinkBatch ]
-        * @param callback $variantLinkCallback Callback to replace findVariantLink() call
+        * @param callable $variantLinkCallback Callback to replace findVariantLink() call
         * @param array $expectedNormal Expected return value of getCategoryLinks['normal']
         * @param array $expectedHidden Expected return value of getCategoryLinks['hidden']
         */
index 7279e64..0e565e5 100644 (file)
@@ -31,7 +31,7 @@ class MutableRevisionSlotsTest extends RevisionSlotsTest {
 
        /**
         * @dataProvider provideConstructorFailue
-        * @param $slots
+        * @param array $slots
         *
         * @covers \MediaWiki\Revision\RevisionSlots::__construct
         * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal
index 43fccee..9665867 100644 (file)
@@ -78,8 +78,8 @@ class RenderedRevisionTest extends MediaWikiTestCase {
        }
 
        /**
-        * @param $articleId
-        * @param $revisionId
+        * @param int $articleId
+        * @param int $revisionId
         * @return Title
         */
        private function getMockTitle( $articleId, $revisionId ) {
index 7188cf5..9f1c69c 100644 (file)
@@ -52,23 +52,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                return $fields;
        }
 
-       protected function getOldCommentQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_comment_text" => "{$prefix}_comment",
-                       "{$prefix}_comment_data" => 'NULL',
-                       "{$prefix}_comment_cid" => 'NULL',
-               ];
-       }
-
-       protected function getCompatCommentQueryFields( $prefix ) {
-               return [
-                       "{$prefix}_comment_text"
-                               => "COALESCE( comment_{$prefix}_comment.comment_text, {$prefix}_comment )",
-                       "{$prefix}_comment_data" => "comment_{$prefix}_comment.comment_data",
-                       "{$prefix}_comment_cid" => "comment_{$prefix}_comment.comment_id",
-               ];
-       }
-
        protected function getNewCommentQueryFields( $prefix ) {
                return [
                        "{$prefix}_comment_text" => "comment_{$prefix}_comment.comment_text",
@@ -106,19 +89,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                ];
        }
 
-       protected function getCompatCommentJoins( $prefix ) {
-               return [
-                       "temp_{$prefix}_comment" => [
-                               "LEFT JOIN",
-                               "temp_{$prefix}_comment.revcomment_{$prefix} = {$prefix}_id",
-                       ],
-                       "comment_{$prefix}_comment" => [
-                               "LEFT JOIN",
-                               "comment_{$prefix}_comment.comment_id = temp_{$prefix}_comment.revcomment_comment_id",
-                       ],
-               ];
-       }
-
        protected function getTextQueryFields() {
                return [
                        'old_text',
@@ -154,7 +124,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                yield 'MCR, comment, actor' => [
                        [
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
                        ],
                        [
@@ -180,7 +149,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage'
                                        => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
                        ],
                        [
@@ -192,11 +160,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'fields' => array_merge(
                                        $this->getArchiveQueryFields( false ),
                                        $this->getNewActorQueryFields( 'ar' ),
-                                       $this->getCompatCommentQueryFields( 'ar' )
+                                       $this->getNewCommentQueryFields( 'ar' )
                                ),
                                'joins' => [
                                        'comment_ar_comment'
-                                               => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+                                               => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
                                        'actor_ar_user' => [ 'JOIN', 'actor_ar_user.actor_id = ar_actor' ],
                                ],
                        ]
@@ -206,7 +174,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage'
                                        => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
                        ],
                        [
@@ -218,11 +185,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        $this->getArchiveQueryFields( true ),
                                        $this->getContentHandlerQueryFields( 'ar' ),
                                        $this->getOldActorQueryFields( 'ar' ),
-                                       $this->getCompatCommentQueryFields( 'ar' )
+                                       $this->getNewCommentQueryFields( 'ar' )
                                ),
                                'joins' => [
                                        'comment_ar_comment'
-                                               => [ 'LEFT JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+                                               => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
                                ],
                        ]
                ];
@@ -230,19 +197,22 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => false,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [
                                'tables' => [
                                        'archive',
+                                       'comment_ar_comment' => 'comment',
                                ],
                                'fields' => array_merge(
                                        $this->getArchiveQueryFields( true ),
                                        $this->getOldActorQueryFields( 'ar' ),
-                                       $this->getOldCommentQueryFields( 'ar' )
+                                       $this->getNewCommentQueryFields( 'ar' )
                                ),
-                               'joins' => [],
+                               'joins' => [
+                                       'comment_ar_comment'
+                                               => [ 'JOIN', 'comment_ar_comment.comment_id = ar_comment_id' ],
+                               ],
                        ]
                ];
        }
@@ -253,7 +223,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
                        ],
                        [ 'page', 'user' ],
@@ -298,7 +267,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage'
                                        => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
                        ],
                        [ 'page', 'user' ],
@@ -317,7 +285,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        $this->getPageQueryFields(),
                                        $this->getUserQueryFields(),
                                        $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => array_merge(
                                        [
@@ -329,9 +297,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                                                'user_id = actor_rev_user.actor_user',
                                                        ]
                                                ],
+                                               'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                               'comment_rev_comment'
+                                                       => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                        ],
-                                       $this->getNewActorJoins( 'rev' ),
-                                       $this->getCompatCommentJoins( 'rev' )
+                                       $this->getNewActorJoins( 'rev' )
                                ),
                        ]
                ];
@@ -340,7 +310,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage'
                                        => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_NEW,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
                        ],
                        [ 'page', 'user' ],
@@ -359,7 +328,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        $this->getPageQueryFields(),
                                        $this->getUserQueryFields(),
                                        $this->getNewActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => array_merge(
                                        [
@@ -371,9 +340,11 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                                                'user_id = actor_rev_user.actor_user'
                                                        ]
                                                ],
+                                               'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                               'comment_rev_comment'
+                                                       => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                        ],
-                                       $this->getNewActorJoins( 'rev' ),
-                                       $this->getCompatCommentJoins( 'rev' )
+                                       $this->getNewActorJoins( 'rev' )
                                ),
                        ]
                ];
@@ -382,7 +353,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage'
                                        => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
                        ],
                        [],
@@ -396,11 +366,13 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        $this->getRevisionQueryFields( true ),
                                        $this->getContentHandlerQueryFields( 'rev' ),
                                        $this->getOldActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
-                       'joins' => array_merge(
-                               $this->getCompatCommentJoins( 'rev' )
-                       ),
+                               'joins' => [
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                               ],
                        ]
                ];
                yield 'MCR write-both/read-old, page, user' => [
@@ -408,7 +380,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage'
                                        => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
                        ],
                        [ 'page', 'user' ],
@@ -426,7 +397,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        $this->getUserQueryFields(),
                                        $this->getPageQueryFields(),
                                        $this->getOldActorQueryFields( 'rev', 'temp_rev_user.revactor_actor' ),
-                                       $this->getCompatCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => array_merge(
                                        [
@@ -438,8 +409,10 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                                                'user_id = rev_user'
                                                        ]
                                                ],
-                                       ],
-                                       $this->getCompatCommentJoins( 'rev' )
+                                               'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                               'comment_rev_comment'
+                                                       => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                                       ]
                                ),
                        ]
                ];
@@ -447,42 +420,55 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [],
                        [
-                               'tables' => [ 'revision' ],
+                               'tables' => [
+                                       'revision',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
                                        $this->getContentHandlerQueryFields( 'rev' ),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
-                               'joins' => [],
+                               'joins' => [
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                               ],
                        ]
                ];
                yield 'pre-MCR, page, user' => [
                        [
                                'wgContentHandlerUseDB' => true,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [ 'page', 'user' ],
                        [
-                               'tables' => [ 'revision', 'page', 'user' ],
+                               'tables' => [
+                                       'revision', 'page', 'user',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
                                        $this->getContentHandlerQueryFields( 'rev' ),
                                        $this->getPageQueryFields(),
                                        $this->getUserQueryFields(),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => [
                                        'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
                                        'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                ],
                        ]
                ];
@@ -490,38 +476,51 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => false,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [],
                        [
-                               'tables' => [ 'revision' ],
+                               'tables' => [
+                                       'revision',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
-                               'joins' => [],
+                               'joins' => [
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
+                               ],
                        ],
                ];
                yield 'pre-MCR, no model, page' => [
                        [
                                'wgContentHandlerUseDB' => false,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [ 'page' ],
                        [
-                               'tables' => [ 'revision', 'page' ],
+                               'tables' => [
+                                       'revision', 'page',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
                                        $this->getPageQueryFields(),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => [
                                        'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ], ],
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                ],
                        ],
                ];
@@ -529,20 +528,26 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => false,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [ 'user' ],
                        [
-                               'tables' => [ 'revision', 'user' ],
+                               'tables' => [
+                                       'revision', 'user',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
                                        $this->getUserQueryFields(),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => [
                                        'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                ],
                        ],
                ];
@@ -550,20 +555,26 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => false,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [ 'text' ],
                        [
-                               'tables' => [ 'revision', 'text' ],
+                               'tables' => [
+                                       'revision', 'text',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
+                               ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
                                        $this->getTextQueryFields(),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => [
                                        'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                ],
                        ],
                ];
@@ -571,13 +582,14 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                        [
                                'wgContentHandlerUseDB' => false,
                                'wgMultiContentRevisionSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        [ 'text', 'page', 'user' ],
                        [
                                'tables' => [
-                                       'revision', 'page', 'user', 'text'
+                                       'revision', 'page', 'user', 'text',
+                                       'temp_rev_comment' => 'revision_comment_temp',
+                                       'comment_rev_comment' => 'comment',
                                ],
                                'fields' => array_merge(
                                        $this->getRevisionQueryFields( true ),
@@ -585,7 +597,7 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        $this->getUserQueryFields(),
                                        $this->getTextQueryFields(),
                                        $this->getOldActorQueryFields( 'rev' ),
-                                       $this->getOldCommentQueryFields( 'rev' )
+                                       $this->getNewCommentQueryFields( 'rev' )
                                ),
                                'joins' => [
                                        'page' => [
@@ -603,6 +615,9 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                                'INNER JOIN',
                                                [ 'rev_text_id=old_id' ],
                                        ],
+                                       'temp_rev_comment' => [ 'JOIN', 'temp_rev_comment.revcomment_rev = rev_id' ],
+                                       'comment_rev_comment'
+                                               => [ 'JOIN', 'comment_rev_comment.comment_id = temp_rev_comment.revcomment_comment_id' ],
                                ],
                        ],
                ];
@@ -833,7 +848,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                yield 'with model, comment, and actor' => [
                        [
                                'wgContentHandlerUseDB' => true,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
                        ],
                        'fields' => array_merge(
@@ -853,7 +867,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                ],
                                $this->getContentHandlerQueryFields( 'rev' ),
                                [
-                                       'rev_comment_old' => 'rev_comment',
                                        'rev_comment_pk' => 'rev_id',
                                ]
                        ),
@@ -861,7 +874,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                yield 'no mode, no comment, no actor' => [
                        [
                                'wgContentHandlerUseDB' => false,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        'fields' => array_merge(
@@ -878,8 +890,8 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        'rev_len',
                                        'rev_parent_id',
                                        'rev_sha1',
-                               ],
-                               $this->getOldCommentQueryFields( 'rev' )
+                                       'rev_comment_pk' => 'rev_id',
+                               ]
                        ),
                ];
        }
@@ -888,7 +900,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                yield 'with model, comment, and actor' => [
                        [
                                'wgContentHandlerUseDB' => true,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_WRITE_BOTH,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
                        ],
                        'fields' => array_merge(
@@ -909,7 +920,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                ],
                                $this->getContentHandlerQueryFields( 'ar' ),
                                [
-                                       'ar_comment_old' => 'ar_comment',
                                        'ar_comment_id' => 'ar_comment_id',
                                ]
                        ),
@@ -917,7 +927,6 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                yield 'no mode, no comment, no actor' => [
                        [
                                'wgContentHandlerUseDB' => false,
-                               'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
                                'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                        ],
                        'fields' => array_merge(
@@ -935,8 +944,8 @@ class RevisionQueryInfoTest extends MediaWikiTestCase {
                                        'ar_len',
                                        'ar_parent_id',
                                        'ar_sha1',
-                               ],
-                               $this->getOldCommentQueryFields( 'ar' )
+                                       'ar_comment_id' => 'ar_comment_id',
+                               ]
                        ),
                ];
        }
index a53cecc..5448320 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 
-// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClassTrait
+// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotClass
 
 namespace MediaWiki\Tests\Revision;
 
@@ -27,7 +27,7 @@ trait RevisionRecordTests {
         *
         * @return RevisionRecord
         */
-       protected abstract function newRevision( array $rowOverrides = [] );
+       abstract protected function newRevision( array $rowOverrides = [] );
 
        private function provideAudienceCheckData( $field ) {
                yield 'field accessible for oversighter (ALL)' => [
index 59b5a2c..071ea68 100644 (file)
@@ -30,8 +30,8 @@ use WikitextContent;
 class RevisionRendererTest extends MediaWikiTestCase {
 
        /**
-        * @param $articleId
-        * @param $revisionId
+        * @param int $articleId
+        * @param int $revisionId
         * @return Title
         */
        private function getMockTitle( $articleId, $revisionId ) {
index d8e7d92..52593ec 100644 (file)
@@ -31,7 +31,7 @@ class RevisionSlotsTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideConstructorFailue
-        * @param $slots
+        * @param array $slots
         *
         * @covers \MediaWiki\Revision\RevisionSlots::__construct
         * @covers \MediaWiki\Revision\RevisionSlots::setSlotsInternal
index 68632f3..018df48 100644 (file)
@@ -80,7 +80,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->setMwGlobals( [
                        'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
                        'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
-                       'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
                        'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                ] );
 
index 2e61745..138d6bc 100644 (file)
@@ -23,6 +23,9 @@ use Wikimedia\TestingAccessWrapper;
 
 class RevisionStoreFactoryTest extends MediaWikiTestCase {
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStoreFactory::__construct
+        */
        public function testValidConstruction_doesntCauseErrors() {
                new RevisionStoreFactory(
                        $this->getMockLoadBalancerFactory(),
@@ -49,6 +52,7 @@ class RevisionStoreFactoryTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideWikiIds
+        * @covers \MediaWiki\Revision\RevisionStoreFactory::getRevisionStore
         */
        public function testGetRevisionStore(
                $wikiId,
index efc2952..c4b274d 100644 (file)
@@ -146,6 +146,9 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
        }
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTitle
+        */
        public function testGetTitle_successFromPageId() {
                $mockLoadBalancer = $this->getMockLoadBalancer();
                // Title calls wfGetDB() so we have to set the main service
@@ -177,6 +180,9 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $this->assertSame( 'Food', $title->getDBkey() );
        }
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTitle
+        */
        public function testGetTitle_successFromPageIdOnFallback() {
                $mockLoadBalancer = $this->getMockLoadBalancer();
                // Title calls wfGetDB() so we have to set the main service
@@ -233,6 +239,9 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $this->assertSame( 'Foodey', $title->getDBkey() );
        }
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTitle
+        */
        public function testGetTitle_successFromRevId() {
                $mockLoadBalancer = $this->getMockLoadBalancer();
                // Title calls wfGetDB() so we have to set the main service
@@ -278,6 +287,9 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $this->assertSame( 'Food2', $title->getDBkey() );
        }
 
+       /**
+        * @covers \MediaWiki\Revision\RevisionStore::getTitle
+        */
        public function testGetTitle_successFromRevIdOnFallback() {
                $mockLoadBalancer = $this->getMockLoadBalancer();
                // Title calls wfGetDB() so we have to set the main service
index a2f2796..339dc30 100644 (file)
@@ -90,7 +90,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $this->setMwGlobals( [
                        'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
                        'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
-                       'wgCommentTableSchemaMigrationStage' => MIGRATION_NEW,
                        'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_OLD,
                ] );
 
@@ -572,28 +571,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                );
        }
 
-       /**
-        * @covers Revision::fetchRevision
-        */
-       public function testFetchRevision() {
-               // Hidden process cache assertion below
-               $this->testPage->getRevision()->getId();
-
-               $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
-               $id = $this->testPage->getRevision()->getId();
-
-               $this->hideDeprecated( 'Revision::fetchRevision' );
-               $res = Revision::fetchRevision( $this->testPage->getTitle() );
-
-               # note: order is unspecified
-               $rows = [];
-               while ( ( $row = $res->fetchObject() ) ) {
-                       $rows[$row->rev_id] = $row;
-               }
-
-               $this->assertEmpty( $rows, 'expected empty set' );
-       }
-
        /**
         * @covers Revision::getPage
         */
index 20689d6..02a6c19 100644 (file)
@@ -281,7 +281,6 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers \MediaWiki\Revision\RevisionStore::newMutableRevisionFromArray
         */
        public function testConstructFromRowWithBadPageId() {
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_NEW );
                $this->overrideMwServices();
                Wikimedia\suppressWarnings();
                $rev = new Revision( (object)[
@@ -602,7 +601,6 @@ class RevisionTest extends MediaWikiTestCase {
         * @covers Revision::loadFromTitle
         */
        public function testLoadFromTitle() {
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_NEW );
                $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
                $this->overrideMwServices();
                $title = $this->getMockTitle();
index 3339749..92c6f62 100644 (file)
@@ -76,7 +76,7 @@ class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
         * Creates a revision in the database.
         *
         * @param WikiPage $page
-        * @param $summary
+        * @param string|Message|CommentStoreComment $summary
         * @param null|string|Content $content
         *
         * @return RevisionRecord|null
index 89e1d4e..9d60605 100644 (file)
@@ -241,7 +241,7 @@ class PageUpdaterTest extends MediaWikiTestCase {
         * Creates a revision in the database.
         *
         * @param WikiPage $page
-        * @param $summary
+        * @param string|Message|CommentStoreComment $summary
         * @param null|string|Content $content
         *
         * @return RevisionRecord|null
index f36fbfd..9c1d5af 100644 (file)
@@ -730,18 +730,6 @@ class TitleTest extends MediaWikiTestCase {
                $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() );
        }
 
-       /**
-        * @dataProvider provideCanHaveTalkPage
-        * @covers Title::canTalk
-        *
-        * @param Title $title
-        * @param bool $expected
-        */
-       public function testCanTalk( Title $title, $expected ) {
-               $actual = $title->canTalk();
-               $this->assertSame( $expected, $actual, $title->getPrefixedDBkey() );
-       }
-
        public static function provideGetTalkPage_good() {
                return [
                        [ Title::makeTitle( NS_MAIN, 'Test' ), Title::makeTitle( NS_TALK, 'Test' ) ],
index 1fb8aff..e87e434 100644 (file)
@@ -249,18 +249,18 @@ class WikiMapTest extends MediaWikiLangTestCase {
 
        /**
         * @dataProvider provideGetWikiIdFromDomain
-        * @covers WikiMap::getWikiIdFromDomain()
+        * @covers WikiMap::getWikiIdFromDbDomain()
         */
        public function testGetWikiIdFromDomain( $domain, $wikiId ) {
-               $this->assertEquals( $wikiId, WikiMap::getWikiIdFromDomain( $domain ) );
+               $this->assertEquals( $wikiId, WikiMap::getWikiIdFromDbDomain( $domain ) );
        }
 
        /**
-        * @covers WikiMap::isCurrentWikiDomain()
-        * @covers WikiMap::getCurrentWikiDomain()
+        * @covers WikiMap::isCurrentWikiDbDomain()
+        * @covers WikiMap::getCurrentWikiDbDomain()
         */
        public function testIsCurrentWikiDomain() {
-               $this->assertTrue( WikiMap::isCurrentWikiDomain( wfWikiID() ) );
+               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( wfWikiID() ) );
 
                $localDomain = DatabaseDomain::newFromId( wfWikiID() );
                $domain1 = new DatabaseDomain(
@@ -268,10 +268,10 @@ class WikiMapTest extends MediaWikiLangTestCase {
                $domain2 = new DatabaseDomain(
                        $localDomain->getDatabase(), null, $localDomain->getTablePrefix() );
 
-               $this->assertTrue( WikiMap::isCurrentWikiDomain( $domain1 ), 'Schema ignored' );
-               $this->assertTrue( WikiMap::isCurrentWikiDomain( $domain2 ), 'Schema ignored' );
+               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $domain1 ), 'Schema ignored' );
+               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $domain2 ), 'Schema ignored' );
 
-               $this->assertTrue( WikiMap::isCurrentWikiDomain( WikiMap::getCurrentWikiDomain() ) );
+               $this->assertTrue( WikiMap::isCurrentWikiDbDomain( WikiMap::getCurrentWikiDbDomain() ) );
        }
 
        public function provideIsCurrentWikiId() {
@@ -294,8 +294,8 @@ class WikiMapTest extends MediaWikiLangTestCase {
        /**
         * @dataProvider provideIsCurrentWikiId
         * @covers WikiMap::isCurrentWikiId()
-        * @covers WikiMap::getCurrentWikiDomain()
-        * @covers WikiMap::getWikiIdFromDomain()
+        * @covers WikiMap::getCurrentWikiDbDomain()
+        * @covers WikiMap::getWikiIdFromDbDomain()
         */
        public function testIsCurrentWikiId( $wikiId, $db, $schema, $prefix ) {
                $this->setMwGlobals(
index 4556473..ab9abbb 100644 (file)
@@ -454,6 +454,34 @@ class XmlTest extends MediaWikiTestCase {
                );
        }
 
+       /**
+        * @covers Xml::encodeJsVar
+        */
+       public function testXmlJsCode() {
+               $code = 'function () { foo( 42 ); }';
+               $this->assertEquals(
+                       $code,
+                       Xml::encodeJsVar( new XmlJsCode( $code ) )
+               );
+       }
+
+       /**
+        * @covers Xml::encodeJsVar
+        * @covers XmlJsCode::encodeObject
+        */
+       public function testEncodeObject() {
+               $codeA = 'function () { foo( 42 ); }';
+               $codeB = 'function ( jQuery ) { bar( 142857 ); }';
+               $obj = XmlJsCode::encodeObject( [
+                       'a' => new XmlJsCode( $codeA ),
+                       'b' => new XmlJsCode( $codeB )
+               ] );
+               $this->assertEquals(
+                       "{\"a\":$codeA,\"b\":$codeB}",
+                       Xml::encodeJsVar( $obj )
+               );
+       }
+
        /**
         * @covers Xml::listDropDown
         */
index 121820a..8049a47 100644 (file)
@@ -529,12 +529,36 @@ class ApiBaseTest extends ApiTestCase {
                                'foo',
                                [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
                        ],
+                       'Deprecated parameter with default, unspecified' => [
+                               null,
+                               [ ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => 'foo' ],
+                               'foo',
+                               [],
+                       ],
+                       'Deprecated parameter with default, specified' => [
+                               'foo',
+                               [ ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => 'foo' ],
+                               'foo',
+                               [ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
+                       ],
                        'Deprecated parameter value' => [
                                'a',
                                [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ],
                                'a',
                                [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
                        ],
+                       'Deprecated parameter value as default, unspecified' => [
+                               null,
+                               [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ], ApiBase::PARAM_DFLT => 'a' ],
+                               'a',
+                               [],
+                       ],
+                       'Deprecated parameter value as default, specified' => [
+                               'a',
+                               [ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ], ApiBase::PARAM_DFLT => 'a' ],
+                               'a',
+                               [ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
+                       ],
                        'Multiple deprecated parameter values' => [
                                'a|b|c|d',
                                [ ApiBase::PARAM_DEPRECATED_VALUES =>
index 7bab542..9e18eb0 100644 (file)
@@ -44,37 +44,37 @@ class ApiComparePagesTest extends ApiTestCase {
                self::$repl['revA2'] = $this->addPage( 'A', 'A 2' );
                self::$repl['revA3'] = $this->addPage( 'A', 'A 3' );
                self::$repl['revA4'] = $this->addPage( 'A', 'A 4' );
-               self::$repl['pageA'] = Title::newFromText( 'ApiComparePagesTest A' )->getArticleId();
+               self::$repl['pageA'] = Title::newFromText( 'ApiComparePagesTest A' )->getArticleID();
 
                self::$repl['revB1'] = $this->addPage( 'B', 'B 1' );
                self::$repl['revB2'] = $this->addPage( 'B', 'B 2' );
                self::$repl['revB3'] = $this->addPage( 'B', 'B 3' );
                self::$repl['revB4'] = $this->addPage( 'B', 'B 4' );
-               self::$repl['pageB'] = Title::newFromText( 'ApiComparePagesTest B' )->getArticleId();
+               self::$repl['pageB'] = Title::newFromText( 'ApiComparePagesTest B' )->getArticleID();
 
                self::$repl['revC1'] = $this->addPage( 'C', 'C 1' );
                self::$repl['revC2'] = $this->addPage( 'C', 'C 2' );
                self::$repl['revC3'] = $this->addPage( 'C', 'C 3' );
-               self::$repl['pageC'] = Title::newFromText( 'ApiComparePagesTest C' )->getArticleId();
+               self::$repl['pageC'] = Title::newFromText( 'ApiComparePagesTest C' )->getArticleID();
 
                $id = $this->addPage( 'D', 'D 1' );
-               self::$repl['pageD'] = Title::newFromText( 'ApiComparePagesTest D' )->getArticleId();
+               self::$repl['pageD'] = Title::newFromText( 'ApiComparePagesTest D' )->getArticleID();
                wfGetDB( DB_MASTER )->delete( 'revision', [ 'rev_id' => $id ] );
 
                self::$repl['revE1'] = $this->addPage( 'E', 'E 1' );
                self::$repl['revE2'] = $this->addPage( 'E', 'E 2' );
                self::$repl['revE3'] = $this->addPage( 'E', 'E 3' );
                self::$repl['revE4'] = $this->addPage( 'E', 'E 4' );
-               self::$repl['pageE'] = Title::newFromText( 'ApiComparePagesTest E' )->getArticleId();
+               self::$repl['pageE'] = Title::newFromText( 'ApiComparePagesTest E' )->getArticleID();
                wfGetDB( DB_MASTER )->update(
                        'page', [ 'page_latest' => 0 ], [ 'page_id' => self::$repl['pageE'] ]
                );
 
                self::$repl['revF1'] = $this->addPage( 'F', "== Section 1 ==\nF 1.1\n\n== Section 2 ==\nF 1.2" );
-               self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleId();
+               self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleID();
 
                self::$repl['revG1'] = $this->addPage( 'G', "== Section 1 ==\nG 1.1", CONTENT_MODEL_TEXT );
-               self::$repl['pageG'] = Title::newFromText( 'ApiComparePagesTest G' )->getArticleId();
+               self::$repl['pageG'] = Title::newFromText( 'ApiComparePagesTest G' )->getArticleID();
 
                WikiPage::factory( Title::newFromText( 'ApiComparePagesTest C' ) )
                        ->doDeleteArticleReal( 'Test for ApiComparePagesTest' );
index d7628e0..2eec176 100644 (file)
@@ -634,6 +634,11 @@ class ApiErrorFormatterTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @covers ApiErrorFormatter::addMessagesFromStatus
+        * @covers ApiErrorFormatter::addWarningOrError
+        * @covers ApiErrorFormatter::formatMessageInternal
+        */
        public function testAddMessagesFromStatus_filter() {
                $result = new ApiResult( 8388608 );
                $formatter = new ApiErrorFormatter( $result, Language::factory( 'qqx' ), 'plaintext', false );
index 3fa8539..1e66a7d 100644 (file)
@@ -36,7 +36,7 @@ class ApiMoveTest extends ApiTestCase {
                        $this->assertSame( $toTitle->getPrefixedText(), $target->getPrefixedText() );
                }
 
-               $this->assertSame( $id, $toTitle->getArticleId() );
+               $this->assertSame( $id, $toTitle->getArticleID() );
        }
 
        /**
@@ -126,7 +126,7 @@ class ApiMoveTest extends ApiTestCase {
                                'to' => '[',
                        ] );
                } finally {
-                       $this->assertSame( $id, Title::newFromText( $name )->getArticleId() );
+                       $this->assertSame( $id, Title::newFromText( $name )->getArticleID() );
                }
        }
 
@@ -161,7 +161,7 @@ class ApiMoveTest extends ApiTestCase {
                                'to' => "$name 3",
                        ] );
                } finally {
-                       $this->assertSame( $id, Title::newFromText( "$name 2" )->getArticleId() );
+                       $this->assertSame( $id, Title::newFromText( "$name 2" )->getArticleID() );
                        $this->assertFalse( Title::newFromText( "$name 3" )->exists(),
                                "\"$name 3\" should not exist" );
                }
@@ -187,7 +187,7 @@ class ApiMoveTest extends ApiTestCase {
                                'tags' => 'custom tag',
                        ] );
                } finally {
-                       $this->assertSame( $id, Title::newFromText( $name )->getArticleId() );
+                       $this->assertSame( $id, Title::newFromText( $name )->getArticleID() );
                        $this->assertFalse( Title::newFromText( "$name 2" )->exists(),
                                "\"$name 2\" should not exist" );
                }
@@ -241,9 +241,9 @@ class ApiMoveTest extends ApiTestCase {
                ] );
 
                $this->assertMoved( $name, "$name 2", $id );
-               $this->assertSame( $talkId, Title::newFromText( "Talk:$name" )->getArticleId() );
+               $this->assertSame( $talkId, Title::newFromText( "Talk:$name" )->getArticleID() );
                $this->assertSame( $talkDestinationId,
-                       Title::newFromText( "Talk:$name 2" )->getArticleId() );
+                       Title::newFromText( "Talk:$name 2" )->getArticleID() );
                $this->assertSame( [ [
                        'message' => 'articleexists',
                        'params' => [],
@@ -278,9 +278,9 @@ class ApiMoveTest extends ApiTestCase {
                }
 
                $this->assertSame( $ids["$name/error"],
-                       Title::newFromText( "$name/error" )->getArticleId() );
+                       Title::newFromText( "$name/error" )->getArticleID() );
                $this->assertSame( $ids["$name 2/error"],
-                       Title::newFromText( "$name 2/error" )->getArticleId() );
+                       Title::newFromText( "$name 2/error" )->getArticleID() );
 
                $results = array_merge( $res[0]['move']['subpages'], $res[0]['move']['subpages-talk'] );
                foreach ( $results as $arr ) {
@@ -317,7 +317,7 @@ class ApiMoveTest extends ApiTestCase {
                                'to' => "$name 2",
                        ], null, $user );
                } finally {
-                       $this->assertSame( $id, Title::newFromText( "$name" )->getArticleId() );
+                       $this->assertSame( $id, Title::newFromText( "$name" )->getArticleID() );
                        $this->assertFalse( Title::newFromText( "$name 2" )->exists(),
                                "\"$name 2\" should not exist" );
                }
@@ -372,7 +372,7 @@ class ApiMoveTest extends ApiTestCase {
                ] );
 
                $this->assertMoved( "Talk:$name", $name, $idBase );
-               $this->assertSame( $idSub, Title::newFromText( "Talk:$name/1" )->getArticleId() );
+               $this->assertSame( $idSub, Title::newFromText( "Talk:$name/1" )->getArticleID() );
                $this->assertFalse( Title::newFromText( "$name/1" )->exists(),
                        "\"$name/1\" should not exist" );
 
index 225c195..d3a4ed4 100644 (file)
@@ -40,36 +40,34 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
        }
 
        public function testLinkPrefixCharset() {
-               global $wgContLang;
-
-               $this->setContentLang( 'ar' );
-               $this->assertTrue( $wgContLang->linkPrefixExtension(), 'Sanity check' );
+               $contLang = Language::factory( 'ar' );
+               $this->setContentLang( $contLang );
+               $this->assertTrue( $contLang->linkPrefixExtension(), 'Sanity check' );
 
                $data = $this->doQuery();
 
-               $this->assertSame( $wgContLang->linkPrefixCharset(), $data['linkprefixcharset'] );
+               $this->assertSame( $contLang->linkPrefixCharset(), $data['linkprefixcharset'] );
        }
 
        public function testVariants() {
-               global $wgContLang;
-
-               $this->setContentLang( 'zh' );
-               $this->assertTrue( $wgContLang->hasVariants(), 'Sanity check' );
+               $contLang = Language::factory( 'zh' );
+               $this->setContentLang( $contLang );
+               $this->assertTrue( $contLang->hasVariants(), 'Sanity check' );
 
                $data = $this->doQuery();
 
                $expected = array_map(
-                       function ( $code ) use ( $wgContLang ) {
-                               return [ 'code' => $code, 'name' => $wgContLang->getVariantname( $code ) ];
+                       function ( $code ) use ( $contLang ) {
+                               return [ 'code' => $code, 'name' => $contLang->getVariantname( $code ) ];
                        },
-                       $wgContLang->getVariants()
+                       $contLang->getVariants()
                );
 
                $this->assertSame( $expected, $data['variants'] );
        }
 
        public function testReadOnly() {
-               $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+               $svc = MediaWikiServices::getInstance()->getReadOnlyMode();
                $svc->setReason( 'Need more donations' );
                try {
                        $data = $this->doQuery();
@@ -82,18 +80,21 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
        }
 
        public function testNamespaces() {
-               global $wgContLang;
-
                $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] );
 
-               $this->assertSame( array_keys( $wgContLang->getFormattedNamespaces() ),
-                       array_keys( $this->doQuery( 'namespaces' ) ) );
+               $this->assertSame(
+                       array_keys( MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces() ),
+                       array_keys( $this->doQuery( 'namespaces' ) )
+               );
        }
 
        public function testNamespaceAliases() {
-               global $wgNamespaceAliases, $wgContLang;
+               global $wgNamespaceAliases;
 
-               $expected = array_merge( $wgNamespaceAliases, $wgContLang->getNamespaceAliases() );
+               $expected = array_merge(
+                       $wgNamespaceAliases,
+                       MediaWikiServices::getInstance()->getContentLanguage()->getNamespaceAliases()
+               );
                $expected = array_map(
                        function ( $key, $val ) {
                                return [ 'id' => $val, 'alias' => strtr( $key, '_', ' ' ) ];
@@ -116,10 +117,8 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
        }
 
        public function testMagicWords() {
-               global $wgContLang;
-
                $this->assertCount(
-                       count( $wgContLang->getMagicWords() ),
+                       count( MediaWikiServices::getInstance()->getContentLanguage()->getMagicWords() ),
                        $this->doQuery( 'magicwords' )
                );
        }
index 95cb3b7..dc7678d 100644 (file)
@@ -38,7 +38,7 @@ class PageRestrictionTest extends RestrictionTestCase {
 
                $restriction = new $class( 1, 1 );
                $title = \Title::newFromId( 1 );
-               $this->assertEquals( $title->getArticleId(), $restriction->getTitle()->getArticleId() );
+               $this->assertEquals( $title->getArticleID(), $restriction->getTitle()->getArticleID() );
        }
 
        public function testNewFromRow() {
index 60f68e7..5df7cca 100644 (file)
@@ -2,6 +2,7 @@
 
 /**
  * @group ContentHandler
+ * @covers MessageContent
  */
 class MessageContentTest extends MediaWikiLangTestCase {
 
index 8e537d6..ecd23f1 100644 (file)
@@ -487,6 +487,10 @@ class TextContentTest extends MediaWikiLangTestCase {
                ];
        }
 
+       /**
+        * @covers TextContent::__construct
+        * @covers TextContentHandler::serializeContent
+        */
        public function testSerialize() {
                $cnt = $this->newContent( 'testing text' );
 
index 5f78a5c..b372e37 100644 (file)
@@ -366,6 +366,9 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase {
                $this->assertEquals( 'This is file content', $data['file_text'] );
        }
 
+       /**
+        * @covers ContentHandler::getSecondaryDataUpdates
+        */
        public function testGetSecondaryDataUpdates() {
                $title = Title::newFromText( 'Somefile.jpg', NS_FILE );
                $content = new WikitextContent( '' );
@@ -379,6 +382,9 @@ class WikitextContentHandlerTest extends MediaWikiLangTestCase {
                $this->assertEquals( [], $updates );
        }
 
+       /**
+        * @covers ContentHandler::getDeletionUpdates
+        */
        public function testGetDeletionUpdates() {
                $title = Title::newFromText( 'Somefile.jpg', NS_FILE );
                $content = new WikitextContent( '' );
index f689cae..2fc7794 100644 (file)
@@ -360,6 +360,10 @@ just a test"
                $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() );
        }
 
+       /**
+        * @covers ParserOptions::getRedirectTarget
+        * @covers ParserOptions::setRedirectTarget
+        */
        public function testRedirectParserOption() {
                $title = Title::newFromText( 'testRedirectParserOption' );
 
index eebb045..e61bd05 100644 (file)
@@ -474,6 +474,9 @@ class DatabaseSqliteTest extends MediaWikiTestCase {
                return $indexes;
        }
 
+       /**
+        * @coversNothing
+        */
        public function testCaseInsensitiveLike() {
                // TODO: Test this for all databases
                $db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
index 1ee188e..a1207b2 100644 (file)
@@ -5,6 +5,7 @@ namespace MediaWiki\Logger\Monolog;
 class LogstashFormatterTest extends \PHPUnit\Framework\TestCase {
        /**
         * @dataProvider provideV1
+        * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1
         * @param array $record The input record.
         * @param array $expected Associative array of expected keys and their values.
         * @param array $notExpected List of keys that should not exist.
@@ -42,6 +43,9 @@ class LogstashFormatterTest extends \PHPUnit\Framework\TestCase {
                ];
        }
 
+       /**
+        * @covers MediaWiki\Logger\Monolog\LogstashFormatter::formatV1
+        */
        public function testV1WithPrefix() {
                $formatter = new LogstashFormatter( 'app', 'system', null, 'ctx_', LogstashFormatter::V1 );
                $record = [ 'extra' => [ 'url' => 1 ], 'context' => [ 'url' => 2 ] ];
index ddc0798..cd3ddfa 100644 (file)
@@ -118,6 +118,8 @@ class LinksUpdateTest extends MediaWikiLangTestCase {
 
        /**
         * @covers ParserOutput::addExternalLink
+        * @covers LinksUpdate::getAddedExternalLinks
+        * @covers LinksUpdate::getRemovedExternalLinks
         */
        public function testUpdate_externallinks() {
                /** @var ParserOutput $po */
@@ -125,7 +127,7 @@ class LinksUpdateTest extends MediaWikiLangTestCase {
 
                $po->addExternalLink( "http://testing.com/wiki/Foo" );
 
-               $this->assertLinksUpdate(
+               $update = $this->assertLinksUpdate(
                        $t,
                        $po,
                        'externallinks',
@@ -135,6 +137,31 @@ class LinksUpdateTest extends MediaWikiLangTestCase {
                                [ 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ],
                        ]
                );
+
+               $this->assertArrayEquals( [
+                       "http://testing.com/wiki/Foo"
+               ], $update->getAddedExternalLinks() );
+
+               $po = new ParserOutput();
+               $po->setTitleText( $t->getPrefixedText() );
+               $po->addExternalLink( 'http://testing.com/wiki/Bar' );
+               $update = $this->assertLinksUpdate(
+                       $t,
+                       $po,
+                       'externallinks',
+                       'el_to, el_index',
+                       'el_from = ' . self::$testingPageId,
+                       [
+                               [ 'http://testing.com/wiki/Bar', 'http://com.testing./wiki/Bar' ],
+                       ]
+               );
+
+               $this->assertArrayEquals( [
+                       "http://testing.com/wiki/Bar"
+               ], $update->getAddedExternalLinks() );
+               $this->assertArrayEquals( [
+                       "http://testing.com/wiki/Foo"
+               ], $update->getRemovedExternalLinks() );
        }
 
        /**
@@ -379,33 +406,17 @@ class LinksUpdateTest extends MediaWikiLangTestCase {
        protected function assertRecentChangeByCategorization(
                Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows
        ) {
-               global $wgCommentTableSchemaMigrationStage;
-
-               if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
-                       $this->assertSelect(
-                               'recentchanges',
-                               'rc_title, rc_comment',
-                               [
-                                       'rc_type' => RC_CATEGORIZE,
-                                       'rc_namespace' => NS_CATEGORY,
-                                       'rc_title' => $categoryTitle->getDBkey()
-                               ],
-                               $expectedRows
-                       );
-               }
-               if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
-                       $this->assertSelect(
-                               [ 'recentchanges', 'comment' ],
-                               'rc_title, comment_text',
-                               [
-                                       'rc_type' => RC_CATEGORIZE,
-                                       'rc_namespace' => NS_CATEGORY,
-                                       'rc_title' => $categoryTitle->getDBkey(),
-                                       'comment_id = rc_comment_id',
-                               ],
-                               $expectedRows
-                       );
-               }
+               $this->assertSelect(
+                       [ 'recentchanges', 'comment' ],
+                       'rc_title, comment_text',
+                       [
+                               'rc_type' => RC_CATEGORIZE,
+                               'rc_namespace' => NS_CATEGORY,
+                               'rc_title' => $categoryTitle->getDBkey(),
+                               'comment_id = rc_comment_id',
+                       ],
+                       $expectedRows
+               );
        }
 
        private function runAllRelatedJobs() {
index e7922fd..4089470 100644 (file)
@@ -5,7 +5,7 @@
  * @covers HTMLCheckMatrix
  */
 class HTMLCheckMatrixTest extends MediaWikiTestCase {
-       static private $defaultOptions = [
+       private static $defaultOptions = [
                'rows' => [ 'r1', 'r2' ],
                'columns' => [ 'c1', 'c2' ],
                'fieldname' => 'test',
index 3b91f5b..892bdcf 100644 (file)
@@ -222,6 +222,9 @@ EOF
 
        /**
         * @dataProvider provideUnknownUserHandling
+        * @covers WikiImporter::setUsernamePrefix
+        * @covers ExternalUserNames::addPrefix
+        * @covers ExternalUserNames::applyPrefix
         * @param bool $assign
         * @param bool $create
         */
index 2760cb9..a6adf34 100644 (file)
@@ -109,6 +109,15 @@ class FormatJsonTest extends MediaWikiTestCase {
                );
        }
 
+       public function testEncodeFail() {
+               // Set up a recursive object that can't be encoded.
+               $a = new stdClass;
+               $b = new stdClass;
+               $a->b = $b;
+               $b->a = $a;
+               $this->assertFalse( FormatJson::encode( $a ) );
+       }
+
        public function testDecodeReturnType() {
                $this->assertInternalType(
                        'object',
index 8a95ae7..0376803 100644 (file)
@@ -138,6 +138,9 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $this->assertSame( 'special', $cache->makeGlobalKey( 'a', 'b' ) );
        }
 
+       /**
+        * @covers MultiWriteBagOStuff::add
+        */
        public function testDuplicateStoreAdd() {
                $bag = new HashBagOStuff();
                $cache = new MultiWriteBagOStuff( [
index 3e52115..369b2bf 100644 (file)
@@ -205,6 +205,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        return $value;
                };
 
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
                $wasSet = 0;
                $v = $cache->getWithSetCallback( $key, 30, $func, [ 'lockTSE' => 5 ] + $extOpts );
                $this->assertEquals( $value, $v, "Value returned" );
@@ -223,10 +227,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( $value, $v, "Value returned" );
                $this->assertEquals( 0, $wasSet, "Value not regenerated" );
 
-               $mockWallClock = microtime( true );
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
                $mockWallClock += 1;
 
                $wasSet = 0;
@@ -284,7 +284,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        return 'xxx' . $wasSet;
                };
 
-               $mockWallClock = microtime( true );
+               $mockWallClock = 1549343530.2053;
                $priorTime = $mockWallClock; // reference time
 
                $wasSet = 0;
@@ -374,7 +374,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
        function testGetWithSetcallback_touched( array $extOpts, $versioned ) {
                $cache = $this->cache;
 
-               $mockWallClock = microtime( true );
+               $mockWallClock = 1549343530.2053;
                $cache->setMockTime( $mockWallClock );
 
                $checkFunc = function ( $oldVal, &$ttl, array $setOpts, $oldAsOf )
@@ -472,7 +472,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        'asyncHandler' => $asyncHandler
                ] );
 
-               $mockWallClock = microtime( true );
+               $mockWallClock = 1549343530.2053;
                $priorTime = $mockWallClock; // reference time
                $cache->setMockTime( $mockWallClock );
 
@@ -573,6 +573,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        return "@$id$";
                };
 
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
                $wasSet = 0;
                $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
                $value = "@3353$";
@@ -602,10 +606,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( 1, $wasSet, "Value not regenerated" );
                $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
 
-               $mockWallClock = microtime( true );
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
                $mockWallClock += 1;
 
                $wasSet = 0;
@@ -746,6 +746,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        return $newValues;
                };
 
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
                $wasSet = 0;
                $keyedIds = new ArrayIterator( [ $keyA => 3353 ] );
                $value = "@3353$";
@@ -773,10 +777,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $this->assertEquals( 1, $wasSet, "Value not regenerated" );
                $this->assertEquals( 0, $cache->getWarmupKeyMisses(), "Keys warmed in process cache" );
 
-               $mockWallClock = microtime( true );
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
                $mockWallClock += 1;
 
                $wasSet = 0;
@@ -1010,6 +1010,10 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $key2 = wfRandomString();
                $key3 = wfRandomString();
 
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $cache->setMockTime( $mockWallClock );
+
                $cache->set( $key1, $value1, 5 );
                $cache->set( $key2, $value2, 10 );
 
@@ -1027,10 +1031,6 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $cKey1 = wfRandomString();
                $cKey2 = wfRandomString();
 
-               $mockWallClock = microtime( true );
-               $priorTime = $mockWallClock; // reference time
-               $cache->setMockTime( $mockWallClock );
-
                $mockWallClock += 1;
 
                $curTTLs = [];
@@ -1074,7 +1074,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $value1 = wfRandomString();
                $value2 = wfRandomString();
 
-               $mockWallClock = microtime( true );
+               $mockWallClock = 1549343530.2053;
                $cache->setMockTime( $mockWallClock );
 
                // Fake initial check key to be set in the past. Otherwise we'd have to sleep for
@@ -1362,7 +1362,7 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $cache = $this->cache;
                $key = wfRandomString();
 
-               $mockWallClock = microtime( true );
+               $mockWallClock = 1549343530.2053;
                $priorTime = $mockWallClock; // reference time
                $cache->setMockTime( $mockWallClock );
 
@@ -1405,18 +1405,22 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                $tKey2 = wfRandomString();
                $value = 'meow';
 
+               $mockWallClock = 1549343530.2053;
+               $priorTime = $mockWallClock; // reference time
+               $this->cache->setMockTime( $mockWallClock );
+
                // Two check keys are newer (given hold-off) than $key, another is older
                $this->internalCache->set(
                        WANObjectCache::TIME_KEY_PREFIX . $tKey2,
-                       WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 3 )
+                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 3 )
                );
                $this->internalCache->set(
                        WANObjectCache::TIME_KEY_PREFIX . $tKey2,
-                       WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 5 )
+                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 5 )
                );
                $this->internalCache->set(
                        WANObjectCache::TIME_KEY_PREFIX . $tKey1,
-                       WANObjectCache::PURGE_VAL_PREFIX . ( microtime( true ) - 30 )
+                       WANObjectCache::PURGE_VAL_PREFIX . ( $priorTime - 30 )
                );
                $this->cache->set( $key, $value, 30 );
 
index b7dbe0b..4c92545 100644 (file)
@@ -672,7 +672,7 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
                $this->assertSame( 'CAST( fieldName AS SIGNED )', $output );
        }
 
-       /*
+       /**
         * @covers Wikimedia\Rdbms\Database::setIndexAliases
         */
        public function testIndexAliases() {
index 4a9603c..d739839 100644 (file)
@@ -311,8 +311,8 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
        /**
         * @covers Wikimedia\Rdbms\Subquery
         * @dataProvider provideSelectRowCount
-        * @param $sql
-        * @param $sqlText
+        * @param array $sql
+        * @param string $sqlText
         */
        public function testSelectRowCount( $sql, $sqlText ) {
                $this->database->selectRowCount(
@@ -740,6 +740,10 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                ];
        }
 
+       /**
+        * @covers Wikimedia\Rdbms\Database::insertSelect
+        * @covers Wikimedia\Rdbms\Database::nativeInsertSelect
+        */
        public function testInsertSelectBatching() {
                $dbWeb = new DatabaseTestHelper( __CLASS__, [ 'cliMode' => false ] );
                $rows = [];
@@ -1874,6 +1878,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
 
        /**
         * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
+        * @covers \Wikimedia\Rdbms\Database::assertTransactionStatus
         */
        public function testTransactionErrorState1() {
                $wrapper = TestingAccessWrapper::newFromObject( $this->database );
index e75b173..b183cde 100644 (file)
@@ -29,18 +29,15 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
         * @param array $selectFields
         * @param string[]|null $row
         * @param string[]|null $expectedFields
-        * @param int $commentMigration
         * @param int $actorMigration
         */
        public function testNewFromId( $id,
                array $selectFields,
                array $row = null,
                array $expectedFields = null,
-               $commentMigration,
                $actorMigration
        ) {
                $this->setMwGlobals( [
-                       'wgCommentTableSchemaMigrationStage' => $commentMigration,
                        'wgActorTableSchemaMigrationStage' => $actorMigration,
                ] );
 
@@ -71,7 +68,10 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
 
        public function provideNewFromId() {
                $oldTables = [
-                       'tables' => [ 'logging', 'user' ],
+                       'tables' => [
+                               'logging', 'user',
+                               'comment_log_comment' => 'comment',
+                       ],
                        'fields' => [
                                'log_id',
                                'log_type',
@@ -84,15 +84,18 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                'user_id',
                                'user_name',
                                'user_editcount',
-                               'log_comment_text' => 'log_comment',
-                               'log_comment_data' => 'NULL',
-                               'log_comment_cid' => 'NULL',
+                               'log_comment_text' => 'comment_log_comment.comment_text',
+                               'log_comment_data' => 'comment_log_comment.comment_data',
+                               'log_comment_cid' => 'comment_log_comment.comment_id',
                                'log_user' => 'log_user',
                                'log_user_text' => 'log_user_text',
                                'log_actor' => 'NULL',
                        ],
                        'options' => [],
-                       'join_conds' => [ 'user' => [ 'LEFT JOIN', 'user_id=log_user' ] ],
+                       'join_conds' => [
+                               'user' => [ 'LEFT JOIN', 'user_id=log_user' ],
+                               'comment_log_comment' => [ 'JOIN', 'comment_log_comment.comment_id = log_comment_id' ],
+                       ],
                ];
                $newTables = [
                        'tables' => [
@@ -133,7 +136,6 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                $oldTables + [ 'conds' => [ 'log_id' => 0 ] ],
                                null,
                                null,
-                               MIGRATION_OLD,
                                SCHEMA_COMPAT_OLD,
                        ],
                        [
@@ -146,7 +148,6 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                        'log_comment_data' => null,
                                ],
                                [ 'type' => 'foobarize', 'comment' => 'test!' ],
-                               MIGRATION_OLD,
                                SCHEMA_COMPAT_OLD,
                        ],
                        [
@@ -159,7 +160,6 @@ class DatabaseLogEntryTest extends MediaWikiTestCase {
                                        'log_comment_data' => null,
                                ],
                                [ 'type' => 'foobarize', 'comment' => 'test!' ],
-                               MIGRATION_NEW,
                                SCHEMA_COMPAT_NEW,
                        ],
                ];
index 6815a62..c0a3e90 100644 (file)
@@ -4,8 +4,6 @@
  *
  * @group Media
  * @group medium
- *
- * @todo covers tags
  */
 class JpegPixelFormatTest extends MediaWikiMediaTestCase {
 
@@ -23,6 +21,7 @@ class JpegPixelFormatTest extends MediaWikiMediaTestCase {
        /**
         *
         * @dataProvider providePixelFormats
+        * @covers BitmapHandler::imageMagickSubsampling
         */
        public function testPixelFormatRendering( $sourceFile, $pixelFormat, $samplingFactor ) {
                global $wgUseImageMagick, $wgUseImageResize;
index 9c98ada..bce7ac2 100644 (file)
@@ -182,7 +182,7 @@ class SvgHandlerTest extends MediaWikiMediaTestCase {
         * @covers SvgHandler::normaliseParamsInternal()
         * @dataProvider provideNormaliseParamsInternal
         *
-        * @param $message
+        * @param string $message
         * @param int $width
         * @param int $height
         * @param array $params
index 8e49bf9..7e43ce4 100644 (file)
@@ -1,7 +1,10 @@
 <?php
+
+use Wikimedia\TestingAccessWrapper;
+
 class ImagePageTest extends MediaWikiMediaTestCase {
 
-       function setUp() {
+       public function setUp() {
                $this->setMwGlobals( 'wgImageLimits', [
                        [ 320, 240 ],
                        [ 640, 480 ],
@@ -12,7 +15,7 @@ class ImagePageTest extends MediaWikiMediaTestCase {
                parent::setUp();
        }
 
-       function getImagePage( $filename ) {
+       public function getImagePage( $filename ) {
                $title = Title::makeTitleSafe( NS_FILE, $filename );
                $file = $this->dataFile( $filename );
                $iPage = new ImagePage( $title );
@@ -26,7 +29,7 @@ class ImagePageTest extends MediaWikiMediaTestCase {
         * @param array $dim Array [maxWidth, maxHeight, width, height]
         * @param array $expected Array [width, height] The width and height we expect to display at
         */
-       function testGetDisplayWidthHeight( $dim, $expected ) {
+       public function testGetDisplayWidthHeight( $dim, $expected ) {
                $iPage = $this->getImagePage( 'animated.gif' );
                $reflection = new ReflectionClass( $iPage );
                $reflMethod = $reflection->getMethod( 'getDisplayWidthHeight' );
@@ -36,7 +39,7 @@ class ImagePageTest extends MediaWikiMediaTestCase {
                $this->assertEquals( $actual, $expected );
        }
 
-       function providerGetDisplayWidthHeight() {
+       public function providerGetDisplayWidthHeight() {
                return [
                        [
                                [ 1024.0, 768.0, 600.0, 600.0 ],
@@ -71,7 +74,7 @@ class ImagePageTest extends MediaWikiMediaTestCase {
         * @param string $filename
         * @param int $expectedNumberThumbs How many thumbnails to show
         */
-       function testGetThumbSizes( $filename, $expectedNumberThumbs ) {
+       public function testGetThumbSizes( $filename, $expectedNumberThumbs ) {
                $iPage = $this->getImagePage( $filename );
                $reflection = new ReflectionClass( $iPage );
                $reflMethod = $reflection->getMethod( 'getThumbSizes' );
@@ -81,7 +84,7 @@ class ImagePageTest extends MediaWikiMediaTestCase {
                $this->assertEquals( count( $actual ), $expectedNumberThumbs );
        }
 
-       function providerGetThumbSizes() {
+       public function providerGetThumbSizes() {
                return [
                        [ 'animated.gif', 2 ],
                        [ 'Toll_Texas_1.svg', 1 ],
@@ -89,4 +92,41 @@ class ImagePageTest extends MediaWikiMediaTestCase {
                        [ 'jpeg-comment-binary.jpg', 2 ],
                ];
        }
+
+       /**
+        * @covers ImagePage::getLanguageForRendering()
+        * @dataProvider provideGetLanguageForRendering
+        *
+        * @param string|null $expected Expected language code
+        * @param string $wikiLangCode Wiki language code
+        * @param string|null $lang lang=... URL parameter
+        */
+       public function testGetLanguageForRendering( $expected = null, $wikiLangCode, $lang = null ) {
+               $params = [];
+               if ( $lang !== null ) {
+                       $params['lang'] = $lang;
+               }
+               $request = new FauxRequest( $params );
+               $this->setMwGlobals( 'wgLanguageCode', $wikiLangCode );
+
+               $page = $this->getImagePage( 'translated.svg' );
+               $page = TestingAccessWrapper::newFromObject( $page );
+
+               /** @var ImagePage $page */
+               $result = $page->getLanguageForRendering( $request, $page->getDisplayedFile() );
+               $this->assertEquals( $expected, $result );
+       }
+
+       public function provideGetLanguageForRendering() {
+               return [
+                       [ 'ru', 'ru' ],
+                       [ 'ru', 'ru', 'ru' ],
+                       [ null, 'en' ],
+                       [ null, 'fr' ],
+                       [ null, 'en', 'en' ],
+                       [ null, 'fr', 'fr' ],
+                       [ null, 'ru', 'en' ],
+                       [ 'de', 'ru', 'de' ],
+               ];
+       }
 }
index 26b6b52..06c0456 100644 (file)
@@ -82,7 +82,6 @@ abstract class PageArchiveTestBase extends MediaWikiTestCase {
 
                $this->tablesUsed += $this->getMcrTablesToReset();
 
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_NEW );
                $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', SCHEMA_COMPAT_OLD );
                $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
                $this->setMwGlobals(
index 298dc52..933e47d 100644 (file)
@@ -1560,89 +1560,6 @@ more stuff
                $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
        }
 
-       /**
-        * @dataProvider provideCommentMigrationOnDeletion
-        *
-        * @param int $writeStage
-        * @param int $readStage
-        */
-       public function testCommentMigrationOnDeletion( $writeStage, $readStage ) {
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $writeStage );
-               $this->overrideMwServices();
-
-               $dbr = wfGetDB( DB_REPLICA );
-
-               $page = $this->createPage(
-                       __METHOD__,
-                       "foo",
-                       CONTENT_MODEL_WIKITEXT
-               );
-               $revid = $page->getLatest();
-               if ( $writeStage > MIGRATION_OLD ) {
-                       $comment_id = $dbr->selectField(
-                               'revision_comment_temp',
-                               'revcomment_comment_id',
-                               [ 'revcomment_rev' => $revid ],
-                               __METHOD__
-                       );
-               }
-
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $readStage );
-               $this->overrideMwServices();
-
-               $page->doDeleteArticle( "testing deletion" );
-
-               if ( $readStage > MIGRATION_OLD ) {
-                       // Didn't leave behind any 'revision_comment_temp' rows
-                       $n = $dbr->selectField(
-                               'revision_comment_temp', 'COUNT(*)', [ 'revcomment_rev' => $revid ], __METHOD__
-                       );
-                       $this->assertEquals( 0, $n, 'no entry in revision_comment_temp after deletion' );
-
-                       // Copied or upgraded the comment_id, as applicable
-                       $ar_comment_id = $dbr->selectField(
-                               'archive',
-                               'ar_comment_id',
-                               [ 'ar_rev_id' => $revid ],
-                               __METHOD__
-                       );
-                       if ( $writeStage > MIGRATION_OLD ) {
-                               $this->assertSame( $comment_id, $ar_comment_id );
-                       } else {
-                               $this->assertNotEquals( 0, $ar_comment_id );
-                       }
-               }
-
-               // Copied rev_comment, if applicable
-               if ( $readStage <= MIGRATION_WRITE_BOTH && $writeStage <= MIGRATION_WRITE_BOTH ) {
-                       $ar_comment = $dbr->selectField(
-                               'archive',
-                               'ar_comment',
-                               [ 'ar_rev_id' => $revid ],
-                               __METHOD__
-                       );
-                       $this->assertSame( 'testing', $ar_comment );
-               }
-       }
-
-       public function provideCommentMigrationOnDeletion() {
-               return [
-                       [ MIGRATION_OLD, MIGRATION_OLD ],
-                       [ MIGRATION_OLD, MIGRATION_WRITE_BOTH ],
-                       [ MIGRATION_OLD, MIGRATION_WRITE_NEW ],
-                       [ MIGRATION_WRITE_BOTH, MIGRATION_OLD ],
-                       [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_BOTH ],
-                       [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ],
-                       [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
-                       [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_BOTH ],
-                       [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_NEW ],
-                       [ MIGRATION_WRITE_NEW, MIGRATION_NEW ],
-                       [ MIGRATION_NEW, MIGRATION_WRITE_BOTH ],
-                       [ MIGRATION_NEW, MIGRATION_WRITE_NEW ],
-                       [ MIGRATION_NEW, MIGRATION_NEW ],
-               ];
-       }
-
        /**
         * @covers WikiPage::updateCategoryCounts
         */
index 390ea41..cb8257c 100644 (file)
@@ -849,6 +849,11 @@ EOF
                $this->assertFieldValues( $a, $expected );
        }
 
+       /**
+        * @covers ParserOutput::mergeInternalMetaDataFrom
+        * @covers ParserOutput::getTimes
+        * @covers ParserOutput::resetParseStartTime
+        */
        public function testMergeInternalMetaDataFrom_parseStartTime() {
                /** @var object $a */
                $a = new ParserOutput();
index be5125c..898ef2d 100644 (file)
@@ -2,6 +2,7 @@
 
 /**
  * @group Parser
+ * @covers MWTidy
  */
 class TidyTest extends MediaWikiTestCase {
 
index 9f9824f..457030f 100644 (file)
@@ -181,6 +181,7 @@ class PasswordPolicyChecksTest extends MediaWikiTestCase {
        /**
         * Verify that all password policy description messages actually exist.
         * Messages used on Special:PasswordPolicies
+        * @coversNothing
         */
        public function testPasswordPolicyDescriptionsExist() {
                global $wgPasswordPolicy;
index 71a3a4f..d5a2b3a 100644 (file)
@@ -357,13 +357,20 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
        /**
         * @dataProvider provideExtractResourceLoaderModules
         */
-       public function testExtractResourceLoaderModules( $input, $expected ) {
+       public function testExtractResourceLoaderModules(
+               $input,
+               array $expectedGlobals,
+               array $expectedAttribs = []
+       ) {
                $processor = new ExtensionProcessor();
                $processor->extractInfo( $this->dir, $input + self::$default, 1 );
                $out = $processor->getExtractedInfo();
-               foreach ( $expected as $key => $value ) {
+               foreach ( $expectedGlobals as $key => $value ) {
                        $this->assertEquals( $value, $out['globals'][$key] );
                }
+               foreach ( $expectedAttribs as $key => $value ) {
+                       $this->assertEquals( $value, $out['attributes'][$key] );
+               }
        }
 
        public static function provideExtractResourceLoaderModules() {
@@ -503,6 +510,27 @@ class ExtensionProcessorTest extends MediaWikiTestCase {
                                        ],
                                ],
                        ],
+                       'QUnit test module' => [
+                               // Input
+                               [
+                                       'QUnitTestModule' => [
+                                               'localBasePath' => '',
+                                               'remoteExtPath' => 'Foo',
+                                               'scripts' => 'bar.js',
+                                       ],
+                               ],
+                               // Expected
+                               [],
+                               [
+                                       'QUnitTestModules' => [
+                                               'test.FooBar' => [
+                                                       'localBasePath' => $dir,
+                                                       'remoteExtPath' => 'Foo',
+                                                       'scripts' => 'bar.js',
+                                               ],
+                                       ],
+                               ],
+                       ],
                ];
        }
 
index dbc757f..70056ba 100644 (file)
@@ -225,7 +225,7 @@ Deprecation message.' ]
                        . '<script>(window.RLQ=window.RLQ||[]).push(function(){'
                        . 'mw.config.set({"key":"value"});'
                        . 'mw.loader.state({"test.exempt":"ready","test.private":"loading","test.styles.pure":"ready","test.styles.private":"ready","test.styles.deprecated":"ready","test.scripts":"loading"});'
-                       . 'mw.loader.implement("test.private@{blankVer}",function($,jQuery,require,module){},{"css":[]});'
+                       . 'mw.loader.implement("test.private@{blankVer}",null,{"css":[]});'
                        . 'RLPAGEMODULES=["test"];mw.loader.load(RLPAGEMODULES);'
                        . 'mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test.scripts\u0026only=scripts\u0026skin=fallback");'
                        . '});</script>' . "\n"
@@ -343,7 +343,7 @@ Deprecation message.' ]
                                'context' => [],
                                'modules' => [ 'test.private' ],
                                'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",function($,jQuery,require,module){},{"css":[]});});</script>',
+                               'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.private@{blankVer}",null,{"css":[]});});</script>',
                        ],
                        [
                                'context' => [],
@@ -393,7 +393,7 @@ Deprecation message.' ]
                                'context' => [],
                                'modules' => [ 'test.shouldembed' ],
                                'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",function($,jQuery,require,module){},{"css":[]});});</script>',
+                               'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
                        ],
                        [
                                'context' => [],
@@ -411,7 +411,7 @@ Deprecation message.' ]
                                'context' => [],
                                'modules' => [ 'test', 'test.shouldembed' ],
                                'only' => ResourceLoaderModule::TYPE_COMBINED,
-                               'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test\u0026skin=fallback");mw.loader.implement("test.shouldembed@09p30q0",function($,jQuery,require,module){},{"css":[]});});</script>',
+                               'output' => '<script>(window.RLQ=window.RLQ||[]).push(function(){mw.loader.load("/w/load.php?debug=false\u0026lang=nl\u0026modules=test\u0026skin=fallback");mw.loader.implement("test.shouldembed@09p30q0",null,{"css":[]});});</script>',
                        ],
                        [
                                'context' => [],
index 20d4b54..fbddfb6 100644 (file)
@@ -320,7 +320,7 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                $testModule = new ResourceLoaderFileModule( [
                        'localBasePath' => $basePath,
                        'styles' => [ 'bom.css' ],
-                       ] );
+               ] );
                $testModule->setName( 'testing' );
                $this->assertEquals(
                        substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
@@ -372,4 +372,211 @@ class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
                        'Using less variables is significant'
                );
        }
+
+       public function providerGetScriptPackageFiles() {
+               $basePath = __DIR__ . '/../../data/resourceloader';
+               $base = [ 'localBasePath' => $basePath ];
+               $commentScript = file_get_contents( "$basePath/script-comment.js" );
+               $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
+               $config = RequestContext::getMain()->getConfig();
+               return [
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'script-comment.js',
+                                               'script-nosemi.js'
+                                       ]
+                               ],
+                               [
+                                       'files' => [
+                                               'script-comment.js' => [
+                                                       'type' => 'script',
+                                                       'content' => $commentScript,
+                                               ],
+                                               'script-nosemi.js' => [
+                                                       'type' => 'script',
+                                                       'content' => $nosemiScript
+                                               ]
+                                       ],
+                                       'main' => 'script-comment.js'
+                               ]
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'script-comment.js',
+                                               'script-nosemi.js' => [ 'main' => true ]
+                                       ],
+                                       'deprecated' => 'Deprecation test',
+                                       'name' => 'test-deprecated'
+                               ],
+                               [
+                                       'files' => [
+                                               'script-comment.js' => [
+                                                       'type' => 'script',
+                                                       'content' => $commentScript,
+                                               ],
+                                               'script-nosemi.js' => [
+                                                       'type' => 'script',
+                                                       'content' => 'mw.log.warn(' .
+                                                               '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
+                                                               "Deprecation test" .
+                                                               '");' .
+                                                               $nosemiScript
+                                               ]
+                                       ],
+                                       'main' => 'script-nosemi.js'
+                               ]
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'init.js' => [ 'file' => 'script-comment.js', 'main' => true ],
+                                               'nosemi.js' => 'script-nosemi.js'
+                                       ]
+                               ],
+                               [
+                                       'files' => [
+                                               'init.js' => [
+                                                       'type' => 'script',
+                                                       'content' => $commentScript,
+                                               ],
+                                               'nosemi.js' => [
+                                                       'type' => 'script',
+                                                       'content' => $nosemiScript
+                                               ]
+                                       ],
+                                       'main' => 'init.js'
+                               ]
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'foo.json' => [ 'content' => [ 'Hello' => 'world' ] ],
+                                               'sample.json',
+                                               'bar.js' => [ 'content' => "console.log('Hello');" ],
+                                               'data' => [ 'type' => 'data', 'callback' => function ( $context ) {
+                                                       return [ 'langCode' => $context->getLanguage() ];
+                                               } ],
+                                               'config' => [ 'type' => 'data', 'config' => [
+                                                       'Sitename',
+                                                       'wgVersion' => 'Version',
+                                               ] ],
+                                       ]
+                               ],
+                               [
+                                       'files' => [
+                                               'foo.json' => [
+                                                       'type' => 'data',
+                                                       'content' => [ 'Hello' => 'world' ],
+                                               ],
+                                               'sample.json' => [
+                                                       'type' => 'data',
+                                                       'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
+                                               ],
+                                               'bar.js' => [
+                                                       'type' => 'script',
+                                                       'content' => "console.log('Hello');",
+                                               ],
+                                               'data' => [
+                                                       'type' => 'data',
+                                                       'content' => [ 'langCode' => 'fy' ]
+                                               ],
+                                               'config' => [
+                                                       'type' => 'data',
+                                                       'content' => [
+                                                               'Sitename' => $config->get( 'Sitename' ),
+                                                               'wgVersion' => $config->get( 'Version' ),
+                                                       ]
+                                               ]
+                                       ],
+                                       'main' => 'bar.js'
+                               ],
+                               [
+                                       'lang' => 'fy'
+                               ]
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               [ 'file' => 'script-comment.js' ]
+                                       ]
+                               ],
+                               false
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'foo.json' => [ 'callback' => 'functionThatDoesNotExist142857' ]
+                                       ]
+                               ],
+                               false
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'foo' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
+                                       ]
+                               ],
+                               false
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'foo.js' => [ 'config' => 'Sitename' ]
+                                       ]
+                               ],
+                               false
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'foo.js' => [ 'garbage' => 'data' ]
+                                       ]
+                               ],
+                               false
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'filethatdoesnotexist142857.js'
+                                       ]
+                               ],
+                               false
+                       ],
+                       [
+                               $base + [
+                                       'packageFiles' => [
+                                               'script-nosemi.js',
+                                               'foo.json' => [
+                                                       'type' => 'data',
+                                                       'content' => [ 'Hello' => 'world' ],
+                                                       'main' => true
+                                               ]
+                                       ]
+                               ],
+                               false
+                       ]
+               ];
+       }
+
+       /**
+        * @dataProvider providerGetScriptPackageFiles
+        * @covers ResourceLoaderFileModule::getScript
+        * @covers ResourceLoaderFileModule::getPackageFiles
+        * @covers ResourceLoaderFileModule::expandPackageFiles
+        */
+       public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
+               $module = new ResourceLoaderFileModule( $moduleDefinition );
+               $context = $this->getResourceLoaderContext( $contextOptions );
+               if ( isset( $moduleDefinition['name'] ) ) {
+                       $module->setName( $moduleDefinition['name'] );
+               }
+               if ( $expected === false ) {
+                       $this->setExpectedException( MWException::class );
+                       $module->getScript( $context );
+               } else {
+                       $this->assertEquals( $expected, $module->getScript( $context ) );
+               }
+       }
 }
index 32afd75..f6bf7f1 100644 (file)
@@ -148,10 +148,6 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
                        'SkinModule (FileModule subclass)' => [ true,
                                [ 'class' => ResourceLoaderSkinModule::class, 'scripts' => 'example.js' ]
                        ],
-                       'JqueryMsgModule (FileModule subclass)' => [ true, [
-                               'class' => ResourceLoaderJqueryMsgModule::class,
-                               'scripts' => 'example.js',
-                       ] ],
                        'WikiModule' => [ false, [
                                'class' => ResourceLoaderWikiModule::class,
                                'scripts' => [ 'MediaWiki:Example.js' ],
@@ -435,6 +431,45 @@ mw.example();
 
                                'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
                        ] ],
+                       [ [
+                               'title' => 'Implement multi-file script',
+
+                               'name' => 'test.multifile',
+                               'scripts' => [
+                                       'files' => [
+                                               'one.js' => [
+                                                       'type' => 'script',
+                                                       'content' => 'mw.example( 1 );',
+                                               ],
+                                               'two.json' => [
+                                                       'type' => 'data',
+                                                       'content' => [ 'n' => 2 ],
+                                               ],
+                                               'three.js' => [
+                                                       'type' => 'script',
+                                                       'content' => 'mw.example( 3 );'
+                                               ],
+                                       ],
+                                       'main' => 'three.js',
+                               ],
+
+                               'expected' => <<<END
+mw.loader.implement( "test.multifile", {
+    "main": "three.js",
+    "files": {
+    "one.js": function ( require, module ) {
+mw.example( 1 );
+},
+    "two.json": {
+    "n": 2
+},
+    "three.js": function ( require, module ) {
+mw.example( 3 );
+}
+}
+} );
+END
+                       ] ],
                ];
        }
 
@@ -446,7 +481,7 @@ mw.example();
        public function testMakeLoaderImplementScript( $case ) {
                $case += [
                        'wrap' => true,
-                       'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
+                       'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [],
                ];
                ResourceLoader::clearCache();
                $this->setMwGlobals( 'wgResourceLoaderDebug', true );
@@ -461,7 +496,8 @@ mw.example();
                                        : $case['scripts'],
                                $case['styles'],
                                $case['messages'],
-                               $case['templates']
+                               $case['templates'],
+                               $case['packageFiles']
                        )
                );
        }
@@ -477,7 +513,8 @@ mw.example();
                        123, // scripts
                        null, // styles
                        null, // messages
-                       null // templates
+                       null, // templates
+                       null // package files
                );
        }
 
index ee272b9..372cb33 100644 (file)
@@ -382,6 +382,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
 
        /**
         * @dataProvider paginationProvider
+        * @covers SearchSuggestionSet::hasMoreResults
         */
        public function testPagination( $hasMoreResults, $provision ) {
                $search = $this->mockSearchWithResults( $provision );
index 54533a7..02fa5e9 100644 (file)
@@ -23,6 +23,7 @@ class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase {
        /**
         * Test that adding a new suggestion at the end
         * will keep proper score ordering
+        * @covers SearchSuggestionSet::append
         */
        public function testAppend() {
                $set = SearchSuggestionSet::emptySuggestionSet();
@@ -54,6 +55,9 @@ class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase {
        /**
         * Test that adding a new best suggestion will keep proper score
         * ordering
+        * @covers SearchSuggestionSet::getWorstScore
+        * @covers SearchSuggestionSet::getBestScore
+        * @covers SearchSuggestionSet::prepend
         */
        public function testInsertBest() {
                $set = SearchSuggestionSet::emptySuggestionSet();
@@ -88,6 +92,9 @@ class SearchSuggestionSetTest extends \PHPUnit\Framework\TestCase {
                $this->assertEquals( $sorted, $scores );
        }
 
+       /**
+        * @covers SearchSuggestionSet::shrink
+        */
        public function testShrink() {
                $set = SearchSuggestionSet::emptySuggestionSet();
                for ( $i = 0; $i < 100; $i++ ) {
index b367979..d198462 100644 (file)
@@ -30,6 +30,9 @@
  */
 class MediaWikiSiteTest extends SiteTest {
 
+       /**
+        * @covers MediaWikiSite::normalizePageName
+        */
        public function testNormalizePageTitle() {
                $this->setMwGlobals( [
                        'wgCapitalLinks' => true,
index 55a8b66..0643e0e 100644 (file)
@@ -29,6 +29,7 @@ class SpecialBlockTest extends SpecialPageTestBase {
        public function testGetFormFields() {
                $this->setMwGlobals( [
                        'wgEnablePartialBlocks' => false,
+                       'wgBlockAllowsUTEdit' => true,
                ] );
                $page = $this->newSpecialPage();
                $wrappedPage = TestingAccessWrapper::newFromObject( $page );
@@ -71,6 +72,7 @@ class SpecialBlockTest extends SpecialPageTestBase {
        public function testMaybeAlterFormDefaults() {
                $this->setMwGlobals( [
                        'wgEnablePartialBlocks' => false,
+                       'wgBlockAllowsUTEdit' => true,
                ] );
 
                $block = $this->insertBlock();
index 6ff2110..1a4fe4f 100644 (file)
@@ -114,6 +114,7 @@ class SpecialSearchTest extends MediaWikiTestCase {
        /**
         * Verify we do not expand search term in <title> on search result page
         * https://gerrit.wikimedia.org/r/4841
+        * @covers SpecialSearch::setupPage
         */
        public function testSearchTermIsNotExpanded() {
                $this->setMwGlobals( [
@@ -175,6 +176,7 @@ class SpecialSearchTest extends MediaWikiTestCase {
 
        /**
         * @dataProvider provideRewriteQueryWithSuggestion
+        * @covers SpecialSearch::showResults
         */
        public function testRewriteQueryWithSuggestion(
                $message,
@@ -224,6 +226,9 @@ class SpecialSearchTest extends MediaWikiTestCase {
                return $mock;
        }
 
+       /**
+        * @covers SpecialSearch::execute
+        */
        public function testSubPageRedirect() {
                $this->setMwGlobals( [
                        'wgScript' => '/w/index.php',
index 570291c..7d37881 100644 (file)
@@ -245,7 +245,7 @@ class BlockListPagerTest extends MediaWikiTestCase {
 
                $restriction = $restrictions[0];
                $this->assertEquals( $page->getId(), $restriction->getValue() );
-               $this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleId() );
+               $this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleID() );
                $this->assertEquals( $title->getDBKey(), $restriction->getTitle()->getDBKey() );
                $this->assertEquals( $title->getNamespace(), $restriction->getTitle()->getNamespace() );
 
index faa9aa1..e576ea8 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 class RemexDriverTest extends MediaWikiTestCase {
-       static private $remexTidyTestData = [
+       private static $remexTidyTestData = [
                // Tests from Html5Depurate
                [
                        'Empty string',
index 84f9378..863ff50 100644 (file)
@@ -583,6 +583,7 @@ class UserTest extends MediaWikiTestCase {
         * When a user is autoblocked a cookie is set with which to track them
         * in case they log out and change IP addresses.
         * @link https://phabricator.wikimedia.org/T5233
+        * @covers User::trackBlockWithCookie
         */
        public function testAutoblockCookies() {
                // Set up the bits of global configuration that we use.
@@ -665,6 +666,7 @@ class UserTest extends MediaWikiTestCase {
        /**
         * Make sure that no cookie is set to track autoblocked users
         * when $wgCookieSetOnAutoblock is false.
+        * @covers User::trackBlockWithCookie
         */
        public function testAutoblockCookiesDisabled() {
                // Set up the bits of global configuration that we use.
@@ -712,6 +714,7 @@ class UserTest extends MediaWikiTestCase {
         * When a user is autoblocked and a cookie is set to track them, the expiry time of the cookie
         * should match the block's expiry, to a maximum of 24 hours. If the expiry time is changed,
         * the cookie's should change with it.
+        * @covers User::trackBlockWithCookie
         */
        public function testAutoblockCookieInfiniteExpiry() {
                $this->setMwGlobals( [
@@ -776,6 +779,9 @@ class UserTest extends MediaWikiTestCase {
                $block->delete();
        }
 
+       /**
+        * @covers User::getBlockedStatus
+        */
        public function testSoftBlockRanges() {
                global $wgUser;
 
@@ -807,6 +813,7 @@ class UserTest extends MediaWikiTestCase {
 
        /**
         * Test that a modified BlockID cookie doesn't actually load the relevant block (T152951).
+        * @covers User::trackBlockWithCookie
         */
        public function testAutoblockCookieInauthentic() {
                // Set up the bits of global configuration that we use.
@@ -853,6 +860,7 @@ class UserTest extends MediaWikiTestCase {
        /**
         * The BlockID cookie is normally verified with a HMAC, but not if wgSecretKey is not set.
         * This checks that a non-authenticated cookie still works.
+        * @covers User::trackBlockWithCookie
         */
        public function testAutoblockCookieNoSecretKey() {
                // Set up the bits of global configuration that we use.
@@ -1022,6 +1030,9 @@ class UserTest extends MediaWikiTestCase {
                $this->assertTrue( User::isLocallyBlockedProxy( $ip ) );
        }
 
+       /**
+        * @covers User::newFromActorId
+        */
        public function testActorId() {
                $domain = MediaWikiServices::getInstance()->getDBLoadBalancer()->getLocalDomainID();
                $this->hideDeprecated( 'User::selectFields' );
@@ -1085,6 +1096,9 @@ class UserTest extends MediaWikiTestCase {
                        'User::newFromActorId works for an anonymous user' );
        }
 
+       /**
+        * @covers User::newFromAnyId
+        */
        public function testNewFromAnyId() {
                // Registered user
                $user = $this->getTestUser()->getUser();
@@ -1271,17 +1285,18 @@ class UserTest extends MediaWikiTestCase {
 
        public static function provideIsBlockedFrom() {
                return [
-                       'Basic operation' => [ 'Test page', true ],
-                       'User talk page, not allowed' => [ self::USER_TALK_PAGE, true, [
+                       'Sitewide block, basic operation' => [ 'Test page', true ],
+                       'Sitewide block, not allowing user talk' => [
+                               self::USER_TALK_PAGE, true, [
                                        'allowUsertalk' => false,
                                ]
                        ],
-                       'User talk page, allowed' => [
-                                       self::USER_TALK_PAGE, false, [
+                       'Sitewide block, allowing user talk' => [
+                               self::USER_TALK_PAGE, false, [
                                        'allowUsertalk' => true,
                                ]
                        ],
-                       'User talk page, allowed but $wgBlockAllowsUTEdit is false' => [
+                       'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [
                                self::USER_TALK_PAGE, true, [
                                        'allowUsertalk' => true,
                                        'blockAllowsUTEdit' => false,
@@ -1297,46 +1312,58 @@ class UserTest extends MediaWikiTestCase {
                                        'pageRestrictions' => [ 'Test page' ],
                                ]
                        ],
-                       'Partial block, allowing user talk' => [
+                       'Partial block, not allowing user talk but user talk page is not blocked' => [
                                self::USER_TALK_PAGE, false, [
                                        'allowUsertalk' => false,
                                        'pageRestrictions' => [ 'Test page' ],
                                ]
                        ],
-                       'Partial block, not allowing user talk' => [
+                       'Partial block, allowing user talk but user talk page is blocked' => [
                                self::USER_TALK_PAGE, true, [
                                        'allowUsertalk' => true,
                                        'pageRestrictions' => [ self::USER_TALK_PAGE ],
                                ]
                        ],
-                       'Partial block, allowing user talk but $wgBlockAllowsUTEdit is false' => [
+                       'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [
                                self::USER_TALK_PAGE, false, [
                                        'allowUsertalk' => false,
                                        'pageRestrictions' => [ 'Test page' ],
                                        'blockAllowsUTEdit' => false,
                                ]
                        ],
-                       'Partial block, not allowing user talk with $wgBlockAllowsUTEdit set to false' => [
+                       'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [
                                self::USER_TALK_PAGE, true, [
                                        'allowUsertalk' => true,
                                        'pageRestrictions' => [ self::USER_TALK_PAGE ],
                                        'blockAllowsUTEdit' => false,
                                ]
                        ],
-                       'Partial namespace block, not allowing user talk' => [ self::USER_TALK_PAGE, true, [
-                               'allowUsertalk' => false,
-                               'namespaceRestrictions' => [ NS_USER_TALK ],
-                       ] ],
-                       'Partial namespace block, not allowing user talk' => [ self::USER_TALK_PAGE, false, [
-                               'allowUsertalk' => true,
-                               'namespaceRestrictions' => [ NS_USER_TALK ],
-                       ] ],
+                       'Partial user talk namespace block, not allowing user talk' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => false,
+                                       'namespaceRestrictions' => [ NS_USER_TALK ],
+                               ]
+                       ],
+                       'Partial user talk namespace block, allowing user talk' => [
+                               self::USER_TALK_PAGE, false, [
+                                       'allowUsertalk' => true,
+                                       'namespaceRestrictions' => [ NS_USER_TALK ],
+                               ]
+                       ],
+                       'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [
+                               self::USER_TALK_PAGE, true, [
+                                       'allowUsertalk' => true,
+                                       'namespaceRestrictions' => [ NS_USER_TALK ],
+                                       'blockAllowsUTEdit' => false,
+                               ]
+                       ],
                ];
        }
 
        /**
         * Block cookie should be set for IP Blocks if
         * wgCookieSetOnIpBlock is set to true
+        * @covers User::trackBlockWithCookie
         */
        public function testIpBlockCookieSet() {
                $this->setMwGlobals( [
@@ -1372,6 +1399,7 @@ class UserTest extends MediaWikiTestCase {
        /**
         * Block cookie should NOT be set when wgCookieSetOnIpBlock
         * is disabled
+        * @covers User::trackBlockWithCookie
         */
        public function testIpBlockCookieNotSet() {
                $this->setMwGlobals( [
@@ -1407,6 +1435,7 @@ class UserTest extends MediaWikiTestCase {
        /**
         * When an ip user is blocked and then they log in, cookie block
         * should be invalid and the cookie removed.
+        * @covers User::trackBlockWithCookie
         */
        public function testIpBlockCookieIgnoredWhenUserLoggedIn() {
                $this->setMwGlobals( [
index b8a60be..38a513e 100644 (file)
@@ -11,8 +11,6 @@ use Title;
 use WikiExporter;
 use WikiPage;
 
-require_once __DIR__ . "/../../../maintenance/dumpTextPass.php";
-
 /**
  * Tests for TextPassDumper that rely on the database
  *
diff --git a/tests/phpunit/maintenance/categoryChangesAsRdfTest.php b/tests/phpunit/maintenance/categoryChangesAsRdfTest.php
new file mode 100644 (file)
index 0000000..f5a47d5
--- /dev/null
@@ -0,0 +1,265 @@
+<?php
+
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Tests for CategoryChangesAsRdf recent changes exporter.
+ *  @covers CategoryChangesAsRdf
+ */
+class CategoryChangesAsRdfTest 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 ],
+                       ],
+                       'edit category' => [
+                               __DIR__ . "/../data/categoriesrdf/edit.sparql",
+                               'getChangedCatsIterator',
+                               'handleEdits',
+                               [
+                                       (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' => 12,
+                                               '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 ],
+                       ],
+                       // TODO: not sure how to test categorization changes, it uses the database select...
+               ];
+       }
+
+       /**
+        * Mock category links iterator.
+        * @param IDatabase $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 );
+       }
+
+}
diff --git a/tests/phpunit/maintenance/categoryChangesRdfTest.php b/tests/phpunit/maintenance/categoryChangesRdfTest.php
deleted file mode 100644 (file)
index 701929a..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-<?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 ],
-                       ],
-                       'edit category' => [
-                               __DIR__ . "/../data/categoriesrdf/edit.sparql",
-                               'getChangedCatsIterator',
-                               'handleEdits',
-                               [
-                                       (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' => 12,
-                                               '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 ],
-                       ],
-                       // TODO: not sure how to test categorization changes, it uses the database select...
-               ];
-       }
-
-       /**
-        * 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 42569d7..b8d1383 100644 (file)
@@ -30,6 +30,9 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                $dbw = wfGetDB( DB_MASTER );
                $logs = [];
 
+               $comment = \MediaWiki\MediaWikiServices::getInstance()->getCommentStore()
+                       ->createComment( $dbw, '' );
+
                // Manual patrolling
                $logs[] = [
                        'log_type' => 'patrol',
@@ -39,6 +42,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20041223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Autopatrol #1
@@ -50,6 +54,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20051223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Block
@@ -61,6 +66,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20061223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Very old/ invalid patrol
@@ -72,6 +78,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20061223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Autopatrol #2
@@ -83,6 +90,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20071223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Autopatrol #3 old way
@@ -94,6 +102,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20081223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Manual patrol #2 old way
@@ -105,6 +114,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20091223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Autopatrol #4 very old way
@@ -116,6 +126,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20081223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                // Manual patrol #3 very old way
@@ -127,6 +138,7 @@ class DeleteAutoPatrolLogsTest extends MaintenanceBaseTestCase {
                        'log_timestamp' => $dbw->timestamp( '20091223210426' ),
                        'log_namespace' => NS_MAIN,
                        'log_title' => 'DeleteAutoPatrolLogs',
+                       'log_comment_id' => $comment->id,
                ];
 
                $dbw->insert( 'logging', $logs );
index 97e0c88..7cbda3c 100644 (file)
@@ -10,8 +10,6 @@ use Title;
 use PHPUnit_Framework_ExpectationFailedException;
 use WikiPage;
 
-require_once __DIR__ . "/../../../maintenance/fetchText.php";
-
 /**
  * Mock for the input/output of FetchText
  *
index ec85bb0..22ca60f 100644 (file)
@@ -170,6 +170,7 @@ class SideBarTest extends MediaWikiLangTestCase {
 
        /**
         * Simple test to verify our helper assertAttribs() is functional
+        * @coversNothing
         */
        public function testTestAttributesAssertionHelper() {
                $this->setMwGlobals( [
index 95d3b60..4c64291 100644 (file)
@@ -10,6 +10,7 @@ use Wikimedia\TestingAccessWrapper;
  * - do not have inconsistencies in the parameter definitions
  *
  * @group API
+ * @coversNothing
  */
 class ApiStructureTest extends MediaWikiTestCase {
 
index b0c1c8f..9c0a73d 100644 (file)
@@ -5,6 +5,7 @@ use Wikimedia\Rdbms\Database;
 
 /**
  * @group Database
+ * @coversNothing
  */
 class DatabaseIntegrationTest extends MediaWikiTestCase {
        /**
index a6bc5a7..c08fe2f 100644 (file)
@@ -11,6 +11,7 @@ use MediaWiki\MediaWikiServices;
  *
  * @since 1.32
  * @author Addshore
+ * @coversNothing
  */
 class SpecialPageFatalTest extends MediaWikiTestCase {
        public function provideSpecialPages() {
index 1118279..cb028a9 100644 (file)
                } );
        } );
 
+       QUnit.test( '.implement( package files )', function ( assert ) {
+               var done = assert.async(),
+                       initJsRan = false;
+               mw.loader.implement(
+                       'test.implement.packageFiles',
+                       {
+                               main: 'resources/src/foo/init.js',
+                               files: {
+                                       'resources/src/foo/data/hello.json': { hello: 'world' },
+                                       'resources/src/foo/foo.js': function ( require, module ) {
+                                               window.mwTestFooJsCounter = window.mwTestFooJsCounter || 41;
+                                               window.mwTestFooJsCounter++;
+                                               module.exports = { answer: window.mwTestFooJsCounter };
+                                       },
+                                       'resources/src/bar/bar.js': function ( require, module ) {
+                                               var core = require( './core.js' );
+                                               module.exports = { data: core.sayHello( 'Alice' ) };
+                                       },
+                                       'resources/src/bar/core.js': function ( require, module ) {
+                                               module.exports = { sayHello: function ( name ) {
+                                                       return 'Hello ' + name;
+                                               } };
+                                       },
+                                       'resources/src/foo/init.js': function ( require ) {
+                                               initJsRan = true;
+                                               assert.deepEqual( require( './data/hello.json' ), { hello: 'world' }, 'require() with .json file' );
+                                               assert.deepEqual( require( './foo.js' ), { answer: 42 }, 'require() with .js file in same directory' );
+                                               assert.deepEqual( require( '../bar/bar.js' ), { data: 'Hello Alice' }, 'require() with ../ of a file that uses same-directory require()' );
+                                               assert.deepEqual( require( './foo.js' ), { answer: 42 }, 'require()ing the same script twice only runs it once' );
+                                       }
+                               }
+                       },
+                       {},
+                       {},
+                       {}
+               );
+               mw.loader.using( 'test.implement.packageFiles' ).done( function () {
+                       assert.ok( initJsRan, 'main JS file is executed' );
+                       done();
+               } );
+       } );
+
        QUnit.test( '.addSource()', function ( assert ) {
                mw.loader.addSource( { testsource1: 'https://1.test/src' } );
 
index ad6a0d0..d22c8d0 100644 (file)
@@ -80,6 +80,7 @@
                },
                teardown: function () {
                        $.fn.updateTooltipAccessKeys.setTestMode( false );
+                       mw.util.resetOptionsForTest();
                },
                messages: {
                        // Used by accessKeyLabel in test for addPortletLink
                        // Distant future: no legacy fallbacks
                        [ allNew, text, html5Encoded ]
                ].forEach( function ( testCase ) {
-                       mw.config.set( 'wgFragmentMode', testCase[ 0 ] );
+                       mw.util.setOptionsForTest( { FragmentMode: testCase[ 0 ] } );
 
                        assert.strictEqual( util.escapeIdForAttribute( testCase[ 1 ] ), testCase[ 2 ] );
                } );
                        // Distant future: no legacy fallbacks
                        [ allNew, text, html5Encoded ]
                ].forEach( function ( testCase ) {
-                       mw.config.set( 'wgFragmentMode', testCase[ 0 ] );
+                       mw.util.setOptionsForTest( { FragmentMode: testCase[ 0 ] } );
 
                        assert.strictEqual( util.escapeIdForLink( testCase[ 1 ] ), testCase[ 2 ] );
                } );
                href = util.getUrl( '#Fragment', { action: 'edit' } );
                assert.strictEqual( href, '/w/index.php?action=edit#Fragment', 'empty title with query string and fragment' );
 
-               mw.config.set( 'wgFragmentMode', [ 'legacy' ] );
+               mw.util.setOptionsForTest( { FragmentMode: [ 'legacy' ] } );
                href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } );
                assert.strictEqual( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_.C3.84', 'title with query string, fragment, and special characters' );
 
-               mw.config.set( 'wgFragmentMode', [ 'html5' ] );
+               mw.util.setOptionsForTest( { FragmentMode: [ 'html5' ] } );
                href = util.getUrl( 'Foo:Sandbox \xC4#Fragment \xC4', { action: 'edit' } );
                assert.strictEqual( href, '/w/index.php?title=Foo:Sandbox_%C3%84&action=edit#Fragment_Ä', 'title with query string, fragment, and special characters' );
 
                href = util.getUrl( 'Foo:%23#Fragment', { action: 'edit' } );
                assert.strictEqual( href, '/w/index.php?title=Foo:%2523&action=edit#Fragment', 'title containing %23 (#), fragment, and a query string' );
 
-               mw.config.set( 'wgFragmentMode', [ 'legacy' ] );
+               mw.util.setOptionsForTest( { FragmentMode: [ 'legacy' ] } );
                href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } );
                assert.strictEqual( href, '/w/index.php?action=edit#.2B.26.3D:.3B.40.24-_..21.2A.2F.5B.5D.3C.3E.27.C2.A7', 'fragment with various characters' );
 
-               mw.config.set( 'wgFragmentMode', [ 'html5' ] );
+               mw.util.setOptionsForTest( { FragmentMode: [ 'html5' ] } );
                href = util.getUrl( '#+&=:;@$-_.!*/[]<>\'§', { action: 'edit' } );
                assert.strictEqual( href, '/w/index.php?action=edit#+&=:;@$-_.!*/[]<>\'§', 'fragment with various characters' );
        } );
index 50ac601..070ad56 100644 (file)
@@ -1,6 +1,43 @@
 const MWBot = require( 'mwbot' ),
        Page = require( './Page' ),
-       FRONTPAGE_REQUESTS_MAX_RUNS = 10; // (arbitrary) safe-guard against endless execution
+       MAINPAGE_REQUESTS_MAX_RUNS = 10; // (arbitrary) safe-guard against endless execution
+
+function getJobCount() {
+       let bot = new MWBot( {
+               apiUrl: `${browser.options.baseUrl}/api.php`
+       } );
+       return bot.request( {
+               action: 'query',
+               meta: 'siteinfo',
+               siprop: 'statistics'
+       } ).then( ( response ) => {
+               return response.query.statistics.jobs;
+       } );
+}
+
+function log( message ) {
+       process.stdout.write( `RunJobs ${message}\n` );
+}
+
+function runThroughMainPageRequests( runCount = 1 ) {
+       let page = new Page();
+       log( `through requests to the main page (run ${runCount}).` );
+
+       page.openTitle( '' );
+
+       return getJobCount().then( ( jobCount ) => {
+               if ( jobCount === 0 ) {
+                       log( 'found no more queued jobs.' );
+                       return;
+               }
+               log( `detected ${jobCount} more queued job(s).` );
+               if ( runCount >= MAINPAGE_REQUESTS_MAX_RUNS ) {
+                       log( 'stopping requests to the main page due to reached limit.' );
+                       return;
+               }
+               return runThroughMainPageRequests( ++runCount );
+       } );
+}
 
 /**
  * Trigger the execution of jobs
@@ -26,48 +63,9 @@ class RunJobs {
 
        static run() {
                browser.call( () => {
-                       return this.runThroughFrontPageRequests();
+                       return runThroughMainPageRequests();
                } );
        }
-
-       static getJobCount() {
-               let bot = new MWBot( {
-                       apiUrl: `${browser.options.baseUrl}/api.php`
-               } );
-               return new Promise( ( resolve ) => {
-                       return bot.request( {
-                               action: 'query',
-                               meta: 'siteinfo',
-                               siprop: 'statistics'
-                       } ).then( ( response ) => {
-                               resolve( response.query.statistics.jobs );
-                       } );
-               } );
-       }
-
-       static runThroughFrontPageRequests( runCount = 1 ) {
-               let page = new Page();
-               this.log( `through requests to the front page (run ${runCount}).` );
-
-               page.openTitle( '' );
-
-               return this.getJobCount().then( ( jobCount ) => {
-                       if ( jobCount === 0 ) {
-                               this.log( 'found no more queued jobs.' );
-                               return;
-                       }
-                       this.log( `detected ${jobCount} more queued job(s).` );
-                       if ( runCount >= FRONTPAGE_REQUESTS_MAX_RUNS ) {
-                               this.log( 'stopping requests to the front page due to reached limit.' );
-                               return;
-                       }
-                       return this.runThroughFrontPageRequests( ++runCount );
-               } );
-       }
-
-       static log( message ) {
-               process.stdout.write( `RunJobs ${message}\n` );
-       }
 }
 
 module.exports = RunJobs;