diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php
index dff3802..a2891f1 100644
--- a/includes/CategoryViewer.php
+++ b/includes/CategoryViewer.php
@@ -32,6 +32,11 @@ class CategoryViewer extends ContextSource {
 	var $collation;
 
 	/**
+	 * @var Collation name
+	 */
+	var $collationName;
+
+	/**
 	 * @var ImageGallery
 	 */
 	var $gallery;
@@ -59,7 +64,7 @@ class CategoryViewer extends ContextSource {
 	 * @param $query Array
 	 */
 	function __construct( $title, IContextSource $context, $from = '', $until = '', $query = array() ) {
-		global $wgCategoryPagingLimit;
+		global $wgCategoryPagingLimit, $wgCategoryCollation;
 		$this->title = $title;
 		$this->setContext( $context );
 		$this->from = $from;
@@ -67,7 +72,14 @@ class CategoryViewer extends ContextSource {
 		$this->limit = $wgCategoryPagingLimit;
 		$this->cat = Category::newFromTitle( $title );
 		$this->query = $query;
-		$this->collation = Collation::singleton();
+		if ( isset( $query['collation'] ) && in_array( $query['collation'], $wgCategoryCollation ) ) {
+			$this->collationName = $query['collation'];
+		} else if ( in_array( $context->getUser()->getOption( 'collation' ), $wgCategoryCollation ) ) {
+			$this->collationName = $context->getUser()->getOption( 'collation' );
+		} else {
+			$this->collationName = $wgCategoryCollation[0];
+		}
+		$this->collation = Collation::singleton( $this->collationName );
 		unset( $this->query['title'] );
 	}
 
@@ -106,6 +118,8 @@ class CategoryViewer extends ContextSource {
 		// Give a proper message if category is empty
 		if ( $r == '' ) {
 			$r = wfMsgExt( 'category-empty', array( 'parse' ) );
+		} else {
+			$r = $this->getCollationSelector() . $r;
 		}
 
 		$lang = $this->getLanguage();
@@ -285,7 +299,11 @@ class CategoryViewer extends ContextSource {
 					'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title',
 					'cat_subcats', 'cat_pages', 'cat_files',
 					'cl_sortkey_prefix', 'cl_collation' ),
-				array_merge( array( 'cl_to' => $this->title->getDBkey() ),  $extraConds ),
+				array_merge(
+					array( 'cl_to' => $this->title->getDBkey() ),
+					array( 'cl_collation IN (' . $dbr->makeList( array( '', $this->collationName ) ) . ')' ),
+					$extraConds
+				),
 				__METHOD__,
 				array(
 					'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
@@ -342,6 +360,42 @@ class CategoryViewer extends ContextSource {
 	/**
 	 * @return string
 	 */
+	function getCollationSelector() {
+		global $wgCategoryCollation;
+
+		if ( count( $wgCategoryCollation ) > 1 ) {
+			$this->getContext()->getOutput()->addModules( 'mediawiki.page.category' );
+			$items = array();
+			foreach ( $wgCategoryCollation as $collation ) {
+				$items[] = Html::element( 'option', array(
+					'value' => $collation,
+				) + (
+					$this->collationName === $collation ? array( 'selected' ) : array()
+				), $this->getContext()->msg( "collation-$collation" )->text() );
+			}
+			$html = Html::rawElement( 'label', array( 'for' => 'mw-collation-select' ),
+				$this->getContext()->msg( 'category-collation' ) );
+			$html .= Html::input( 'title', $this->title->getPrefixedText(), 'hidden' );
+			$html .= Html::rawElement( 'select', array(
+				'name' => 'collation',
+				'id' => 'mw-collation-select',
+			), implode( $items ) );
+			$html .= Html::input( 'go', $this->getContext()->msg( 'go' )->text(), 'submit', array(
+				'id' => 'mw-collation-go',
+			) );
+			return Html::rawElement( 'form', array(
+				'action' => wfScript(),
+				'method' => 'get',
+				'id' => 'mw-collation-selector'
+			), $html );
+		} else {
+			return '';
+		}
+	}
+
+	/**
+	 * @return string
+	 */
 	function getSubcategorySection() {
 		# Don't show subcategories section if there are none.
 		$r = '';
diff --git a/includes/Collation.php b/includes/Collation.php
index e4ffae6..0f404d1 100644
--- a/includes/Collation.php
+++ b/includes/Collation.php
@@ -1,17 +1,26 @@
 <?php
 
 abstract class Collation {
-	static $instance;
+	static $instance = array();
 
 	/**
 	 * @return Collation
 	 */
-	static function singleton() {
-		if ( !self::$instance ) {
-			global $wgCategoryCollation;
-			self::$instance = self::factory( $wgCategoryCollation );
+	static function singleton( $name = null ) {
+		global $wgCategoryCollation;
+
+		if ( !isset( $name ) ) {
+			foreach ( $wgCategoryCollation as $name ) {
+				self::singleton( $name );
+			}
+			return self::$instance;
+		}
+		if ( !isset( self::$instance[$name] ) ) {
+			if ( in_array( $name, $wgCategoryCollation ) ) {
+				self::$instance[$name] = self::factory( $name );
+			}
 		}
-		return self::$instance;
+		return self::$instance[$name];
 	}
 
 	/**
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index e214b7b..b728f26 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -5022,7 +5022,7 @@ $wgCategoryPagingLimit = 200;
  * Extensions can define there own collations by subclassing Collation
  * and using the Collation::factory hook.
  */
-$wgCategoryCollation = 'uppercase';
+$wgCategoryCollation = array( 'uppercase' );
 
 /** @} */ # End categories }
 
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index 716e7d8..09dd14e 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -477,7 +477,7 @@ class LinksUpdate {
 	 * @return array
 	 */
 	private function getCategoryInsertions( $existing = array() ) {
-		global $wgContLang, $wgCategoryCollation;
+		global $wgContLang;
 		$diffs = array_diff_assoc( $this->mCategories, $existing );
 		$arr = array();
 		foreach ( $diffs as $name => $prefix ) {
@@ -492,22 +492,24 @@ class LinksUpdate {
 				$type = 'page';
 			}
 
-			# Treat custom sortkeys as a prefix, so that if multiple
-			# things are forced to sort as '*' or something, they'll
-			# sort properly in the category rather than in page_id
-			# order or such.
-			$sortkey = Collation::singleton()->getSortKey(
-				$this->mTitle->getCategorySortkey( $prefix ) );
+			foreach ( Collation::singleton() as $collationName => $collation ) {
+				# Treat custom sortkeys as a prefix, so that if multiple
+				# things are forced to sort as '*' or something, they'll
+				# sort properly in the category rather than in page_id
+				# order or such.
+				$sortkey = $collation->getSortKey(
+					$this->mTitle->getCategorySortkey( $prefix ) );
 
-			$arr[] = array(
-				'cl_from'    => $this->mId,
-				'cl_to'      => $name,
-				'cl_sortkey' => $sortkey,
-				'cl_timestamp' => $this->mDb->timestamp(),
-				'cl_sortkey_prefix' => $prefix,
-				'cl_collation' => $wgCategoryCollation,
-				'cl_type' => $type,
-			);
+				$arr[] = array(
+					'cl_from'    => $this->mId,
+					'cl_to'      => $name,
+					'cl_sortkey' => $sortkey,
+					'cl_timestamp' => $this->mDb->timestamp(),
+					'cl_sortkey_prefix' => $prefix,
+					'cl_collation' => $collationName,
+					'cl_type' => $type,
+				);
+			}
 		}
 		return $arr;
 	}
diff --git a/includes/Preferences.php b/includes/Preferences.php
index bc8e47f..e140c71 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -134,9 +134,9 @@ class Preferences {
 	 */
 	static function profilePreferences( $user, IContextSource $context, &$defaultPreferences ) {
 		global $wgAuth, $wgContLang, $wgParser, $wgCookieExpiration, $wgLanguageCode,
-			$wgDisableTitleConversion, $wgDisableLangConversion, $wgMaxSigChars,
-			$wgEnableEmail, $wgEmailConfirmToEdit, $wgEnableUserEmail, $wgEmailAuthentication,
-			$wgEnotifWatchlist, $wgEnotifUserTalk, $wgEnotifRevealEditorAddress;
+			$wgDisableTitleConversion, $wgDisableLangConversion, $wgMaxSigChars, $wgEnableEmail,
+			$wgEmailConfirmToEdit, $wgEnableUserEmail, $wgEmailAuthentication, $wgEnotifWatchlist,
+			$wgEnotifUserTalk, $wgEnotifRevealEditorAddress, $wgCategoryCollation;
 
 		## User info #####################################
 		// Information panel
@@ -297,6 +297,21 @@ class Preferences {
 			}
 		}
 
+		/* Check if there are more than one collations configured on this site */
+		if ( count( $wgCategoryCollation ) > 1 ) {
+			$options = array();
+			foreach ( $wgCategoryCollation as $collation ) {
+				$options[$context->msg( "collation-$collation" )->text()] = $collation;
+			}
+			$defaultPreferences['collation'] = array(
+				'label-message' => 'prefs-collation',
+				'type' => 'select',
+				'options' => $options,
+				'section' => 'personal/i18n',
+				'help-message' => 'prefs-help-collation',
+			);
+		}
+
 		if ( count( $variantArray ) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion ) {
 			$defaultPreferences['noconvertlink'] =
 				array(
diff --git a/includes/Setup.php b/includes/Setup.php
index 2777f73..afe3668 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -348,6 +348,14 @@ if ( $wgAjaxUploadDestCheck ) {
 	$wgAjaxExportList[] = 'SpecialUpload::ajaxGetExistsWarning';
 }
 
+if ( !is_array( $wgCategoryCollation ) ) {
+	$wgCategoryCollation = array( $wgCategoryCollation );
+}
+
+if ( !isset( $wgDefaultUserOptions['collation'] ) ) {
+	$wgDefaultUserOptions['collation'] = $wgCategoryCollation[0];
+}
+
 if ( $wgNewUserLog ) {
 	# Add a new log type
 	$wgLogTypes[]                        = 'newusers';
diff --git a/includes/Title.php b/includes/Title.php
index 769adb9..09c1e4a 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -3499,16 +3499,22 @@ class Title {
 		foreach ( $prefixes as $prefixRow ) {
 			$prefix = $prefixRow->cl_sortkey_prefix;
 			$catTo = $prefixRow->cl_to;
-			$dbw->update( 'categorylinks',
-				array(
-					'cl_sortkey' => Collation::singleton()->getSortKey(
-						$nt->getCategorySortkey( $prefix ) ),
-					'cl_timestamp=cl_timestamp' ),
-				array(
-					'cl_from' => $pageid,
-					'cl_to' => $catTo ),
-				__METHOD__
-			);
+			foreach ( Collation::singleton() as $collationName => $collation ) {
+				$dbw->update( 'categorylinks',
+					array(
+						'cl_sortkey' => $collation->getSortKey(
+							$nt->getCategorySortkey( $prefix )
+						),
+						'cl_timestamp=cl_timestamp',
+					),
+					array(
+						'cl_from' => $pageid,
+						'cl_to' => $catTo,
+						'cl_collation' => $collationName,
+					),
+					__METHOD__
+				);
+			}
 		}
 
 		$redirid = $this->getArticleID();
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 4b19b7e..afeaace 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -52,6 +52,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 	 * @return void
 	 */
 	private function run( $resultPageSet = null ) {
+		global $wgCategoryCollation;
+
 		$params = $this->extractRequestParams();
 
 		$this->requireOnlyOneParameter( $params, 'title', 'pageid' );
@@ -72,6 +74,13 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 			}
 		}
 
+		if ( isset( $params['collation'] ) ) {
+			$collation = $params['collation'];
+		} else if ( in_array( $this->getContext()->getUser()->getOption( 'collation' ), $wgCategoryCollation ) ) {
+			$collation = $this->getContext()->getUser()->getOption( 'collation' );
+		} else {
+			$collation = $wgCategoryCollation[0];
+		}
 		$prop = array_flip( $params['prop'] );
 		$fld_ids = isset( $prop['ids'] );
 		$fld_title = isset( $prop['title'] );
@@ -94,6 +103,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 		$this->addTables( array( 'page', 'categorylinks' ) );	// must be in this order for 'USE INDEX'
 
 		$this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() );
+		$this->addWhere( 'cl_collation IN (' . $this->getDB()->makeList( array( '', $collation ) ) . ')' );
 		$queryTypes = $params['type'];
 		$contWhere = false;
 
@@ -143,10 +153,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 				$this->addWhereRange( 'cl_from', $dir, null, null );
 			} else {
 				$startsortkey = $params['startsortkeyprefix'] !== null ?
-					Collation::singleton()->getSortkey( $params['startsortkeyprefix'] ) :
+					Collation::singleton( $collation )->getSortkey( $params['startsortkeyprefix'] ) :
 					$params['startsortkey'];
 				$endsortkey = $params['endsortkeyprefix'] !== null ?
-					Collation::singleton()->getSortkey( $params['endsortkeyprefix'] ) :
+					Collation::singleton( $collation )->getSortkey( $params['endsortkeyprefix'] ) :
 					$params['endsortkey'];
 
 				// The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them
@@ -265,6 +275,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 	}
 
 	public function getAllowedParams() {
+		global $wgCategoryCollation;
+
 		return array(
 			'title' => array(
 				ApiBase::PARAM_TYPE => 'string',
@@ -284,6 +296,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 					'timestamp',
 				)
 			),
+			'collation' => array(
+				ApiBase::PARAM_TYPE => $wgCategoryCollation
+			),
 			'namespace' => array (
 				ApiBase::PARAM_ISMULTI => true,
 				ApiBase::PARAM_TYPE => 'namespace',
@@ -347,6 +362,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 				' type          - Adds the type that the page has been categorised as (page, subcat or file)',
 				' timestamp     - Adds the timestamp of when the page was included',
 			),
+			'collation' => 'Collation to use',
 			'namespace' => 'Only include pages in these namespaces',
 			'type' => "What type of category members to include. Ignored when {$p}sort=timestamp is set",
 			'sort' => 'Property to sort by',
diff --git a/includes/installer/DatabaseUpdater.php b/includes/installer/DatabaseUpdater.php
index 6ff0af9..53f1152 100644
--- a/includes/installer/DatabaseUpdater.php
+++ b/includes/installer/DatabaseUpdater.php
@@ -683,7 +683,7 @@ abstract class DatabaseUpdater {
 		if ( $this->db->selectField(
 			'categorylinks',
 			'COUNT(*)',
-			'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
+			'cl_collation NOT IN (' . $this->db->makeList( $wgCategoryCollation ) . ')',
 			__METHOD__
 		) == 0 ) {
 			$this->output( "...collations up-to-date.\n" );
diff --git a/includes/installer/MysqlUpdater.php b/includes/installer/MysqlUpdater.php
index 5e6ae7e..6914784 100644
--- a/includes/installer/MysqlUpdater.php
+++ b/includes/installer/MysqlUpdater.php
@@ -196,6 +196,7 @@ class MysqlUpdater extends DatabaseUpdater {
 			// 1.20
 			array( 'addTable', 'config',                            'patch-config.sql' ),
 			array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
+			array( 'doCategorylinksCollationIndicesUpdate' ),
 		);
 	}
 
@@ -737,6 +738,15 @@ class MysqlUpdater extends DatabaseUpdater {
 		}
 	}
 
+	protected function doCategorylinksCollationIndicesUpdate() {
+		if ( !$this->indexHasField( 'categorylinks', 'cl_from', 'cl_collation' ) ||
+			!$this->indexHasField( 'categorylinks', 'cl_sortkey', 'cl_collation' )
+		) {
+			$this->applyPatch( 'patch-categorylinks-multiple-collations.sql' );
+			$this->output( "...categorylinks collation indices updated\n" );
+		}
+	}
+
 	protected function doCategoryPopulation() {
 		if ( $this->updateRowExists( 'populate category' ) ) {
 			$this->output( "...category table already populated.\n" );
diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php
index 933cff1..63f95e9 100644
--- a/languages/messages/MessagesEn.php
+++ b/languages/messages/MessagesEn.php
@@ -746,6 +746,7 @@ XHTML id names.
 'category-empty'                 => "''This category currently contains no pages or media.''",
 'hidden-categories'              => '{{PLURAL:$1|Hidden category|Hidden categories}}',
 'hidden-category-category'       => 'Hidden categories',
+'category-collation'             => 'Collation: ',
 'category-subcat-count'          => '{{PLURAL:$2|This category has only the following subcategory.|This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}, out of $2 total.}}',
 'category-subcat-count-limited'  => 'This category has the following {{PLURAL:$1|subcategory|$1 subcategories}}.',
 'category-article-count'         => '{{PLURAL:$2|This category contains only the following page.|The following {{PLURAL:$1|page is|$1 pages are}} in this category, out of $2 total.}}',
@@ -1850,6 +1851,11 @@ This cannot be undone.',
 'yourlanguage'                  => 'Language:',
 'yourvariant'                   => 'Content language variant:',
 'prefs-help-variant'            => 'Your preferred variant or orthography to display the content pages of this wiki in.',
+'prefs-collation'               => 'Collation:',
+'prefs-help-collation'          => 'Your preferred way to sort pages in categories.',
+'collation-uppercase'           => 'Uppercase',
+'collation-identity'            => 'Identity',
+'collation-uca-default'         => 'UCA (Default)',
 'yournick'                      => 'New signature:',
 'prefs-help-signature'          => 'Comments on talk pages should be signed with "<nowiki>~~~~</nowiki>" which will be converted into your signature and a timestamp.',
 'badsig'                        => 'Invalid raw signature.
diff --git a/maintenance/archives/patch-categorylinks-multiple-collations.sql b/maintenance/archives/patch-categorylinks-multiple-collations.sql
new file mode 100644
index 0000000..90b2216
--- /dev/null
+++ b/maintenance/archives/patch-categorylinks-multiple-collations.sql
@@ -0,0 +1,13 @@
+-- 
+-- patch-categorylinks-multiple-collations.sql
+--
+-- Allows more than one collations to be used at the same time
+-- 
+
+ALTER TABLE /*_*/categorylinks
+   DROP INDEX /*i*/cl_from,
+   ADD UNIQUE INDEX /*i*/cl_from(cl_from,cl_to,cl_collation);
+
+ALTER TABLE /*_*/categorylinks
+   DROP INDEX /*i*/cl_sortkey,
+   ADD INDEX /*i*/cl_sortkey(cl_to,cl_type,cl_collation,cl_sortkey,cl_from);
diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc
index c9afbfa..eb389b2 100644
--- a/maintenance/language/messages.inc
+++ b/maintenance/language/messages.inc
@@ -147,6 +147,7 @@ $wgMessageStructure = array(
 		'category-empty',
 		'hidden-categories',
 		'hidden-category-category',
+		'category-collation',
 		'category-subcat-count',
 		'category-subcat-count-limited',
 		'category-article-count',
@@ -1006,6 +1007,11 @@ $wgMessageStructure = array(
 		'yourlanguage',
 		'yourvariant',
 		'prefs-help-variant',
+		'prefs-collation',
+		'prefs-help-collation',
+		'collation-uppercase',
+		'collation-identity',
+		'collation-uca-default',
 		'yournick',
 		'prefs-help-signature',
 		'badsig',
diff --git a/maintenance/tables.sql b/maintenance/tables.sql
index 60fc7fc..a9b5e50 100644
--- a/maintenance/tables.sql
+++ b/maintenance/tables.sql
@@ -537,12 +537,12 @@ CREATE TABLE /*_*/categorylinks (
   cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page'
 ) /*$wgDBTableOptions*/;
 
-CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to);
+CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to,cl_collation);
 
 -- We always sort within a given category, and within a given type.  FIXME:
 -- Formerly this index didn't cover cl_type (since that didn't exist), so old
 -- callers won't be using an index: fix this?
-CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from);
+CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_collation,cl_sortkey,cl_from);
 
 -- Not really used?
 CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp);
diff --git a/maintenance/updateCollation.php b/maintenance/updateCollation.php
index 6160a30..cbe8791 100644
--- a/maintenance/updateCollation.php
+++ b/maintenance/updateCollation.php
@@ -45,59 +45,43 @@ TEXT;
 
 		$this->addOption( 'force', 'Run on all rows, even if the collation is ' .
 			'supposed to be up-to-date.' );
-		$this->addOption( 'previous-collation', 'Set the previous value of ' .
-			'$wgCategoryCollation here to speed up this script, especially if your ' .
-			'categorylinks table is large. This will only update rows with that ' .
-			'collation, though, so it may miss out-of-date rows with a different, ' .
-			'even older collation.', false, true );
 	}
 
 	public function execute() {
-		global $wgCategoryCollation, $wgMiserMode;
+		global $wgCategoryCollation;
 
 		$dbw = $this->getDB( DB_MASTER );
 		$force = $this->getOption( 'force' );
 
-		$options = array( 'LIMIT' => self::BATCH_SIZE, 'STRAIGHT_JOIN' );
+		$options = array( 'LIMIT' => self::BATCH_SIZE, 'DISTINCT', 'STRAIGHT_JOIN' );
+
+		$collationConds = array();
 
 		if ( $force ) {
 			$options['ORDER BY'] = 'cl_from, cl_to';
-			$collationConds = array();
 		} else {
-			if ( $this->hasOption( 'previous-collation' ) ) {
-				$collationConds['cl_collation'] = $this->getOption( 'previous-collation' );
-			} else {
-				$collationConds = array( 0 =>
-					'cl_collation != ' . $dbw->addQuotes( $wgCategoryCollation )
-				);
-			}
-
-			if ( !$wgMiserMode ) {
-				$count = $dbw->selectField(
-					'categorylinks',
-					'COUNT(*)',
-					$collationConds,
-					__METHOD__
-				);
-
-				if ( $count == 0 ) {
-					$this->output( "Collations up-to-date.\n" );
-					return;
-				}
-				$this->output( "Fixing collation for $count rows.\n" );
-			}
+			$collationConds[] = '(' . $dbw->selectSQLText(
+				array( 'cli' => 'categorylinks' ),
+				'COUNT(*)',
+				array(
+					'cli.cl_from = clo.cl_from',
+					'cli.cl_to = clo.cl_to',
+					'cli.cl_collation IN (' . $dbw->makeList( $wgCategoryCollation ) . ')',
+				),
+				__METHOD__
+			) . ') < ' . count( $wgCategoryCollation );
 		}
 
 		$count = 0;
 		$batchCount = 0;
 		$batchConds = array();
 		do {
-			$this->output( "Selecting next " . self::BATCH_SIZE . " rows..." );
+			$this->output( "Selecting next " . self::BATCH_SIZE . " pairs..." );
+
+			# Select from/to pairs that don't have enough categorylinks rows
 			$res = $dbw->select(
-				array( 'categorylinks', 'page' ),
-				array( 'cl_from', 'cl_to', 'cl_sortkey_prefix', 'cl_collation',
-					'cl_sortkey', 'page_namespace', 'page_title'
-				),
+				array( 'clo' => 'categorylinks', 'page' ),
+				array( 'cl_from', 'cl_to', 'page_namespace', 'page_title' ),
 				array_merge( $collationConds, $batchConds, array( 'cl_from = page_id' ) ),
 				__METHOD__,
 				$options
@@ -107,19 +91,44 @@ TEXT;
 			$dbw->begin( __METHOD__ );
 			foreach ( $res as $row ) {
 				$title = Title::newFromRow( $row );
-				if ( !$row->cl_collation ) {
-					# This is an old-style row, so the sortkey needs to be
-					# converted.
-					if ( $row->cl_sortkey == $title->getText()
-						|| $row->cl_sortkey == $title->getPrefixedText() ) {
-						$prefix = '';
+				# Try to extract some data about the given pair
+				$res2 = $dbw->select(
+					'categorylinks',
+					array( 'cl_sortkey_prefix', 'cl_collation', 'cl_sortkey', 'cl_timestamp' ),
+					array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ),
+					__METHOD__
+				);
+
+				$prefix = null;
+				$prefix2 = null;
+				$timestamp = null;
+				$collations = array();
+				foreach ( $res2 as $row2 ) {
+					$collations[] = $row2->cl_collation;
+					if ( !$row2->cl_collation ) {
+						# This is an old-style row, so the sortkey needs to be
+						# converted.
+						if ( $row2->cl_sortkey == $title->getText()
+							|| $row2->cl_sortkey == $title->getPrefixedText() ) {
+							$prefix = '';
+						} else {
+							# Custom sortkey, use it as a prefix
+							$prefix = $row2->cl_sortkey;
+						}
 					} else {
-						# Custom sortkey, use it as a prefix
-						$prefix = $row->cl_sortkey;
+						$prefix2 = $row2->cl_sortkey_prefix;
+					}
+					if ( isset( $timestamp ) ) {
+						# Should we compare timestamp values more carefully?
+						$timestamp = min( $timestamp, $row2->cl_timestamp );
+					} else {
+						$timestamp = $row2->cl_timestamp;
 					}
-				} else {
-					$prefix = $row->cl_sortkey_prefix;
 				}
+				if ( isset( $prefix2 ) ) {
+					$prefix = $prefix2;
+				}
+
 				# cl_type will be wrong for lots of pages if cl_collation is 0,
 				# so let's update it while we're here.
 				if ( $title->getNamespace() == NS_CATEGORY ) {
@@ -129,19 +138,52 @@ TEXT;
 				} else {
 					$type = 'page';
 				}
-				$dbw->update(
+
+				$dbw->delete(
 					'categorylinks',
 					array(
-						'cl_sortkey' => Collation::singleton()->getSortKey(
-							$title->getCategorySortkey( $prefix ) ),
-						'cl_sortkey_prefix' => $prefix,
-						'cl_collation' => $wgCategoryCollation,
-						'cl_type' => $type,
-						'cl_timestamp = cl_timestamp',
+						'cl_from' => $row->cl_from,
+						'cl_to' => $row->cl_to,
+						'cl_collation NOT IN (' . $dbw->makeList( $wgCategoryCollation ) . ')',
 					),
-					array( 'cl_from' => $row->cl_from, 'cl_to' => $row->cl_to ),
 					__METHOD__
 				);
+
+				foreach ( Collation::singleton() as $collationName => $collation ) {
+					if ( !in_array( $collationName, $collations ) ) {
+						$dbw->insert(
+							'categorylinks',
+							array(
+								'cl_from' => $row->cl_from,
+								'cl_to' => $row->cl_to,
+								'cl_sortkey' => $collation->getSortKey(
+									$title->getCategorySortkey( $prefix ) ),
+								'cl_sortkey_prefix' => $prefix,
+								'cl_collation' => $collationName,
+								'cl_type' => $type,
+								'cl_timestamp' => $timestamp,
+							),
+							__METHOD__
+						);
+					} else if ( $force ) {
+						$dbw->update(
+							'categorylinks',
+							array(
+								'cl_sortkey' => $collation->getSortKey(
+									$title->getCategorySortkey( $prefix ) ),
+								'cl_sortkey_prefix' => $prefix,
+								'cl_type' => $type,
+								'cl_timestamp' => $timestamp,
+							),
+							array(
+								'cl_from' => $row->cl_from,
+								'cl_to' => $row->cl_to,
+								'cl_collation' => $collationName,
+							),
+							__METHOD__
+						);
+					}
+				}
 			}
 			$dbw->commit( __METHOD__ );
 
diff --git a/resources/Resources.php b/resources/Resources.php
index 4e4c90a..d1352bd 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -751,7 +751,11 @@ return array(
 		),
 		'position' => 'top',
 	),
-
+	'mediawiki.page.category' => array(
+		'scripts' => 'resources/mediawiki.page/mediawiki.page.category.js',
+		'styles' => 'resources/mediawiki.page/mediawiki.page.category.css',
+		'position' => 'top',
+	),
 
 	/* MediaWiki Special pages */
 
diff --git a/resources/mediawiki.page/mediawiki.page.category.css b/resources/mediawiki.page/mediawiki.page.category.css
new file mode 100644
index 0000000..0d75cdf
--- /dev/null
+++ b/resources/mediawiki.page/mediawiki.page.category.css
@@ -0,0 +1,3 @@
+#mw-collation-selector {
+	float: right;
+}
diff --git a/resources/mediawiki.page/mediawiki.page.category.js b/resources/mediawiki.page/mediawiki.page.category.js
new file mode 100644
index 0000000..1eab400
--- /dev/null
+++ b/resources/mediawiki.page/mediawiki.page.category.js
@@ -0,0 +1,9 @@
+jQuery( document ).ready( function( $ ) {
+
+	$( '#mw-collation-select' ).change( function() {
+		$( '#mw-collation-selector' )[0].submit();
+	} );
+
+	$( '#mw-collation-go' ).hide();
+
+} );
