﻿Index: trunk/phase3/skins/monobook/main.css
===================================================================
--- trunk/phase3/skins/monobook/main.css	(revision 50159)
+++ trunk/phase3/skins/monobook/main.css	(working copy)
@@ -1379,26 +1379,20 @@
 	padding: 0.2em;
 }
 
-
 /* Allmessages table */
-
-#allmessagestable th {
-	background-color: #b2b2ff;
+#allmessagestable .allmessages-customised td.am_default {
+	background-color: #fcffc4;
 }
-
-#allmessagestable tr.orig {
-	background-color: #ffe2e2;
+#allmessagestable tr.allmessages-customised:hover td.am_default {
+	background-color: #faff90;
 }
-
-#allmessagestable tr.new {
+#allmessagestable td.am_actual {
 	background-color: #e2ffe2;
 }
-
-#allmessagestable tr.def {
-	background-color: #f0f0ff;
+#allmessagestable tr.allmessages-customised:hover + tr.allmessages-customised td.am_actual {
+	background-color: #b1ffb1;
 }
 
-
 /* noarticletext */
 div.noarticletext {
 	border: 1px solid #ccc;
Index: trunk/phase3/skins/common/allmessages.js
===================================================================
--- trunk/phase3/skins/common/allmessages.js	(revision 50159)
+++ trunk/phase3/skins/common/allmessages.js	(working copy)
@@ -1,83 +0,0 @@
-var allmessages_nodelist = false;
-var allmessages_modified = false;
-var allmessages_timeout = false;
-var allmessages_running = false;
-
-function allmessagesmodified() {
-	allmessages_modified = !allmessages_modified;
-	allmessagesfilter();
-}
-
-function allmessagesfilter() {
-	if ( allmessages_timeout )
-		window.clearTimeout( allmessages_timeout );
-
-	if ( !allmessages_running )
-		allmessages_timeout = window.setTimeout( 'allmessagesfilter_do();', 500 );
-}
-
-function allmessagesfilter_do() {
-	if ( !allmessages_nodelist )
-		return;
-
-	var text = document.getElementById('allmessagesinput').value.toLowerCase();
-	var nodef = allmessages_modified;
-
-	allmessages_running = true;
-
-	for ( var name in allmessages_nodelist ) {
-		var nodes = allmessages_nodelist[name];
-		var display = ( name.toLowerCase().indexOf( text ) == -1 ? 'none' : '' );
-
-		for ( var i = 0; i < nodes.length; i++)
-			nodes[i].style.display =
-				( nodes[i].className == "def" && nodef
-				  ? 'none' : display );
-	}
-
-	if ( text != document.getElementById('allmessagesinput').value.toLowerCase() ||
-	     nodef != allmessages_modified )
-		allmessagesfilter_do();  // repeat
-
-	allmessages_running = false;
-}
-
-function allmessagesfilter_init() {
-	if ( allmessages_nodelist )
-		return;
-
-	var nodelist = new Array();
-	var templist = new Array();
-
-	var table = document.getElementById('allmessagestable');
-	if ( !table ) return;
-
-	var rows = document.getElementsByTagName('tr');
-	for ( var i = 0; i < rows.length; i++ ) {
-		var id = rows[i].getAttribute('id')
-		if ( id && id.substring(0,16) != 'sp-allmessages-r' ) continue;
-		templist[ id ] = rows[i];
-	}
-
-	var spans = table.getElementsByTagName('span');
-	for ( var i = 0; i < spans.length; i++ ) {
-		var id = spans[i].getAttribute('id')
-		if ( id && id.substring(0,17) != 'sp-allmessages-i-' ) continue;
-		if ( !spans[i].firstChild || spans[i].firstChild.nodeType != 3 ) continue;
-
-		var nodes = new Array();
-		var row1 = templist[ id.replace('i', 'r1') ];
-		var row2 = templist[ id.replace('i', 'r2') ];
-
-		if ( row1 ) nodes[nodes.length] = row1;
-		if ( row2 ) nodes[nodes.length] = row2;
-		nodelist[ spans[i].firstChild.nodeValue ] = nodes;
-	}
-
-	var k = document.getElementById('allmessagesfilter');
-	if (k) { k.style.display = ''; }
-
-	allmessages_nodelist = nodelist;
-}
-
-hookEvent( "load", allmessagesfilter_init );
Index: trunk/phase3/includes/AutoLoader.php
===================================================================
--- trunk/phase3/includes/AutoLoader.php	(revision 50159)
+++ trunk/phase3/includes/AutoLoader.php	(working copy)
@@ -449,6 +449,7 @@
 	'MWTidy' => 'includes/parser/Tidy.php',
 
 	# includes/specials
+	'SpecialAllmessages' => 'includes/specials/SpecialAllmessages.php',
 	'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php',
 	'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php',
 	'ContribsPager' => 'includes/specials/SpecialContributions.php',
Index: trunk/phase3/includes/DefaultSettings.php
===================================================================
--- trunk/phase3/includes/DefaultSettings.php	(revision 50159)
+++ trunk/phase3/includes/DefaultSettings.php	(working copy)
@@ -1477,7 +1477,7 @@
  * to ensure that client-side caches don't keep obsolete copies of global
  * styles.
  */
-$wgStyleVersion = '217';
+$wgStyleVersion = '218';
 
 
 # Server-side caching:
Index: trunk/phase3/includes/specials/SpecialAllmessages.php
===================================================================
--- trunk/phase3/includes/specials/SpecialAllmessages.php	(revision 50159)
+++ trunk/phase3/includes/specials/SpecialAllmessages.php	(working copy)
@@ -4,234 +4,373 @@
  * @file
  * @ingroup SpecialPage
  */
-
-/**
- * Constructor.
- */
-function wfSpecialAllmessages() {
-	global $wgOut, $wgRequest, $wgMessageCache;
-	global $wgUseDatabaseMessages, $wgLang;
-
-	# The page isn't much use if the MediaWiki namespace is not being used
-	if( !$wgUseDatabaseMessages ) {
-		$wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
-		return;
+class SpecialAllmessages extends SpecialPage {
+	
+	/**
+	 * Constructor
+	 */
+	public function __construct() {
+		parent::__construct( 'Allmessages' );
 	}
+	
+	/**
+	 * Execute
+	 */
+	function execute( $par ) {
+		global $wgOut, $wgRequest;
+		
+		$this->setHeaders();
+		
+		global $wgUseDatabaseMessages;
+		if( !$wgUseDatabaseMessages ) {
+			$wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
+			return;
+		} else {
+			$this->outputHeader( 'allmessagestext' );
+		}
 
-	wfProfileIn( __METHOD__ );
-
-	wfProfileIn( __METHOD__ . '-setup' );
-	$ot = $wgRequest->getText( 'ot' );
-
-	$navText = wfMsg( 'allmessagestext' );
-
-	# Make sure all extension messages are available
-
-	$wgMessageCache->loadAllMessages();
-
-	$sortedArray = array_merge( Language::getMessagesFor( 'en' ), 
-		$wgMessageCache->getExtensionMessagesFor( 'en' ) );
-	ksort( $sortedArray );
-
-	$messages = array();
-	foreach( $sortedArray as $key => $value ) {
-		$messages[$key]['enmsg'] = $value;
-		$messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false );
-		$messages[$key]['msg'] = wfMsgNoTrans( $key );
-		$sortedArray[$key] = NULL; // trade bytes from $sortedArray to this
+		$this->filter = $wgRequest->getVal( 'filter', 'all' );
+		$this->prefix = $wgRequest->getVal( 'prefix', '' );
 		
+		$this->table = new AllmessagesTablePager( $this,
+											$conds=array(),
+											wfGetLangObj( $wgRequest->getVal( 'lang', false ) ) );
+											
+		$this->langCode = $this->table->lang->getCode();
+		
+		$wgOut->addHTML( $this->buildForm() .
+						 $this->table->getNavigationBar() . 
+						 $this->table->getLimitForm() . 
+						 $this->table->getBody() . 
+						 $this->table->getNavigationBar() );
+		
 	}
-	unset($sortedArray); // trade bytes from $sortedArray to this
-
-	wfProfileOut( __METHOD__ . '-setup' );
-
-	wfProfileIn( __METHOD__ . '-output' );
-	$wgOut->addScriptFile( 'allmessages.js' );
-	$title = SpecialPage::getTitleFor( 'Allmessages' );
-	if ( $ot == 'php' ) {
-		$navText .= wfAllMessagesMakePhp( $messages );
-		$wgOut->addHTML( $wgLang->pipeList( array(
-			'PHP',
-			'<a href="' . $title->escapeLocalUrl( 'ot=html' ) . '">HTML</a>',
-			'<a href="' . $title->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' .
-			'<pre>' . htmlspecialchars( $navText ) . '</pre>'
-		) ) );
-	} else if ( $ot == 'xml' ) {
-		$wgOut->disable();
-		header( 'Content-type: text/xml' );
-		echo wfAllMessagesMakeXml( $messages );
-	} else {
-		$wgOut->addHTML( $wgLang->pipeList( array(
-			'<a href="' . $title->escapeLocalUrl( 'ot=php' ) . '">PHP</a>',
-			'HTML',
-			'<a href="' . $title->escapeLocalUrl( 'ot=xml' ) . '">XML</a>'
-		) ) );
-		$wgOut->addWikiText( $navText );
-		$wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) );
+	
+	function buildForm() {
+		$url = $this->getTitle()->escapeLocalURL();
+		$languages = Language::getLanguageNames( false );
+		ksort( $languages );
+		
+		$out  = "<form method=\"get\" action=\"$url\"><fieldset>\n" .
+					Xml::element( 'legend', null, wfMsg( 'allmessages' ) ) . "<table><tr>\n" .
+				"<td class=\"mw-label\">" .
+	                Xml::label( wfMsg('allmessages-prefix'), 'am-form-prefix' ) .
+	            "</td>\n<td class=\"mw-input\">" .
+	                Xml::input( 'prefix', 20, str_replace('_',' ',$this->prefix), array( 'id' => 'am-form-prefix' ) ) . 
+	            "</select>" . 
+				"</td>\n</tr><tr>\n<td class='mw-label'>" . 
+	                Xml::label( wfMsg('allmessages-filter'), 'am-form-filter' ) .
+	            "</td>\n<td class='mw-input'>" . 
+	                Xml::radioLabel( wfMsg('allmessages-filter-unmodified'),
+	                				 'filter',
+	                				 'unmodified',
+	                				 'am-form-filter-unmodified',
+	                				 ( $this->filter == 'unmodified' ? true : false )
+	                				) . 
+	                Xml::radioLabel( wfMsg('allmessages-filter-all'),
+	                				 'filter',
+	                				 'all',
+	                				 'am-form-filter-all',
+	                				 ( $this->filter == 'all' ? true : false )
+	                				) . 
+	                Xml::radioLabel( wfMsg('allmessages-filter-modified'),
+	                				 'filter',
+	                				 'modified',
+	                				 'am-form-filter-modified',
+	                				 ( $this->filter == 'modified' ? true : false )
+	                				) .
+				"</td>\n</tr><tr>\n<td class=\"mw-label\">" .
+	                Xml::label( wfMsg('yourlanguage'), 'am-form-lang' ) .
+	            "</td>\n<td class=\"mw-input\">" .
+	                Xml::openElement( 'select', array( 'id' => 'am-form-lang', 'name' => 'lang' ) );
+		foreach( $languages as $lang => $name ) {
+			$selected = $lang == $this->langCode ? 'selected="selected"' : '';
+			$out .= "<option value=\"$lang\" $selected>$name</option>\n";
+		}
+		$out .= "</td>\n</tr><tr>\n<td></td><td>" . Xml::submitButton( wfMsg('allpagessubmit') ) . 
+				"</table>" .
+					$this->table->getHiddenFields( array( 'title', 'prefix', 'filter', 'lang' ) ) .
+				"</fieldset></form>";
+		return $out;
 	}
-	wfProfileOut( __METHOD__ . '-output' );
-
-	wfProfileOut( __METHOD__ );
 }
 
-function wfAllMessagesMakeXml( &$messages ) {
-	global $wgLang;
-	$lang = $wgLang->getCode();
-	$txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
-	$txt .= "<messages lang=\"$lang\">\n";
-	foreach( $messages as $key => $m ) {
-		$txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n";
-		$messages[$key] = NULL; // trade bytes
-	}
-	$txt .= "</messages>";
-	return $txt;
-}
-
-/**
- * Create the messages array, formatted in PHP to copy to language files.
- * @param $messages Messages array.
- * @return The PHP messages array.
- * @todo Make suitable for language files.
+/* use TablePager for prettified output. We have to pretend that we're 
+ * getting data from a table when in fact not all of it comes from the database.
  */
-function wfAllMessagesMakePhp( &$messages ) {
-	global $wgLang;
-	$txt = "\n\n\$messages = array(\n";
-	foreach( $messages as $key => $m ) {
-		if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) {
-			continue;
-		} else if ( wfEmptyMsg( $key, $m['msg'] ) ) {
-			$m['msg'] = '';
-			$comment = ' #empty';
+class AllmessagesTablePager extends TablePager {
+	
+	var $messages  = NULL;
+	var $talkPages = NULL;
+	
+	function __construct( $page, $conds, $langObj = NULL ) {
+		parent::__construct();
+		$this->mIndexField = 'am_title';
+		$this->mPage = $page;
+		$this->mConds = $conds;
+		$this->mDefaultDirection = true;	//always sort ascending
+		
+		global $wgLang, $wgContLang, $wgRequest;
+		
+		$this->talk = $wgLang->lc( htmlspecialchars( wfMsg( 'talkpagelinktext' ) ) );
+		
+		$this->lang = ( $langObj ? $langObj : $wgContLang );
+		$this->langcode = $this->lang->getCode();
+		$this->foreign  = $this->langcode != $wgContLang->getCode();
+		
+		if( $wgRequest->getVal( 'filter', 'all' ) === 'all' ){
+			$this->custom = NULL;	//So won't match in either case
 		} else {
-			$comment = '';
+			$this->custom = $wgRequest->getVal( 'filter' ) == 'unmodified' ? 1 : 0;
 		}
-		$txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
-		$messages[$key] = NULL; // trade bytes
+		
+		$prefix = $wgLang->ucfirst( $wgRequest->getVal( 'prefix', '' ) );
+		$prefix = $prefix != '' ? Title::makeTitleSafe( NS_MEDIAWIKI, $wgRequest->getVal( 'prefix', NULL ) ) : NULL;
+		if( $prefix !== NULL ){
+			$this->prefix = '/^' . preg_quote( $prefix->getDBkey() ) . '/i';
+		} else {
+			$this->prefix = false;
+		}
+		$this->getSkin();
+		
+		//The suffix that may be needed for message names if we're in a 
+		//different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr'
+		if( $this->foreign ) {
+			$this->suffix = '/' . $this->langcode;
+		} else { 
+			$this->suffix = '';
+		}
 	}
-	$txt .= ');';
-	return $txt;
-}
-
-/**
- * Create a list of messages, formatted in HTML as a list of messages and values and showing differences between the default language file message and the message in MediaWiki: namespace.
- * @param $messages Messages array.
- * @return The HTML list of messages.
- */
-function wfAllMessagesMakeHTMLText( &$messages ) {
-	global $wgLang, $wgContLang, $wgUser;
-	wfProfileIn( __METHOD__ );
-
-	$sk = $wgUser->getSkin();
-	$talk = wfMsg( 'talkpagelinktext' );
-
-	$input = Xml::element( 'input', array(
-		'type'    => 'text',
-		'id'      => 'allmessagesinput',
-		'onkeyup' => 'allmessagesfilter()'
-	), '' );
-	$checkbox = Xml::element( 'input', array(
-		'type'    => 'button',
-		'value'   => wfMsgHtml( 'allmessagesmodified' ),
-		'id'      => 'allmessagescheckbox',
-		'onclick' => 'allmessagesmodified()'
-	), '' );
-
-	$txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . 
-		" {$input}{$checkbox} " . '</span>';
-
-	$txt .= '
-<table border="1" cellspacing="0" width="100%" id="allmessagestable">
-	<tr>
-		<th rowspan="2">' . wfMsgHtml( 'allmessagesname' ) . '</th>
-		<th>' . wfMsgHtml( 'allmessagesdefault' ) . '</th>
-	</tr>
-	<tr>
-		<th>' . wfMsgHtml( 'allmessagescurrent' ) . '</th>
-	</tr>';
-
-	wfProfileIn( __METHOD__ . "-check" );
-
-	# This is a nasty hack to avoid doing independent existence checks
-	# without sending the links and table through the slow wiki parser.
-	$pageExists = array(
-		NS_MEDIAWIKI => array(),
-		NS_MEDIAWIKI_TALK => array()
-	);
-	$dbr = wfGetDB( DB_SLAVE );
-	$res = $dbr->select( 'page',
-		array( 'page_namespace', 'page_title' ),
-		array( 'page_namespace' => array(NS_MEDIAWIKI,NS_MEDIAWIKI_TALK) ),
-		__METHOD__,
-		array( 'USE INDEX' => 'name_title' )
-	);
-	while( $s = $dbr->fetchObject( $res ) ) {
-		$pageExists[$s->page_namespace][$s->page_title] = 1;
+	
+	function getAllMessages( $desc ){
+		
+		wfProfileIn( __METHOD__ . '-cache' );
+		
+		# Make sure all extension messages are available
+		global $wgMessageCache;
+		$wgMessageCache->loadAllMessages( 'en' );
+		$sortedArray = array_merge( Language::getMessagesFor( 'en' ), 
+									$wgMessageCache->getExtensionMessagesFor( 'en' ) );
+		if( $desc ){
+			krsort( $sortedArray );
+		} else {
+			ksort( $sortedArray );
+		}
+		
+		$this->messages = array();
+		foreach( $sortedArray as $key => $value ) {
+			// All messages start with lowercase, but wikis might have both
+			// upper and lowercase MediaWiki: pages if $wgCapitalLinks=false.
+			$ukey = $this->lang->ucfirst( $key );
+			
+			// The value without any overrides from the MediaWiki: namespace
+			$this->messages[$ukey]['default'] = wfMsgGetKey( $key, /*useDB*/false, $this->langcode, false );
+			
+			// The message that's actually used by the site
+			$this->messages[$ukey]['actual'] = wfMsgGetKey( $key, /*useDB*/true, $this->langcode, false );	
+			
+			$this->messages[$ukey]['customised'] = 0; //for now
+			
+			$sortedArray[$key] = NULL; // trade bytes from $sortedArray to this	
+		}
+	
+		wfProfileOut( __METHOD__ . '-cache' );
+		
+		return true;
 	}
-	$dbr->freeResult( $res );
-	wfProfileOut( __METHOD__ . "-check" );
+	
+	# We only need a list of which messages have *been* customised; 
+	# their content is already in the message cache.
+	function markCustomisedMessages(){
+		$this->talkPages = array();
+		
+		wfProfileIn( __METHOD__ . "-db" );
 
-	wfProfileIn( __METHOD__ . "-output" );
-
-	$i = 0;
-
-	foreach( $messages as $key => $m ) {
-		$title = $wgLang->ucfirst( $key );
-		if( $wgLang->getCode() != $wgContLang->getCode() ) {
-			$title .= '/' . $wgLang->getCode();
+		$dbr = wfGetDB( DB_SLAVE );
+		$res = $dbr->select( 'page',
+							 array( 'page_namespace', 'page_title' ),
+							 array( 'page_namespace' => array(NS_MEDIAWIKI,NS_MEDIAWIKI_TALK) ),
+							 __METHOD__,
+							 array( 'USE INDEX' => 'name_title' )
+						   );
+		
+		while( $s = $dbr->fetchObject( $res ) ) {
+			if( $s->page_namespace == NS_MEDIAWIKI ){
+				if( $this->foreign ){
+					$title = explode( '/', $s->page_title );
+					if( $this->langcode == $title[1] && array_key_exists( $title[0], $this->messages ) ){
+						$this->messages["{$title[0]}"]['customised'] = 1;
+				    }
+				} else if( array_key_exists( $s->page_title , $this->messages ) ){
+					$this->messages[$s->page_title]['customised'] = 1;
+				}
+			} else if( $s->page_namespace == NS_MEDIAWIKI_TALK ){
+				$this->talkPages[$s->page_title] = 1;
+			}
 		}
-
-		$titleObj = Title::makeTitle( NS_MEDIAWIKI, $title );
-		$talkPage = Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
-
-		$changed = ( $m['statmsg'] != $m['msg'] );
-		$message = htmlspecialchars( $m['statmsg'] );
-		$mw = htmlspecialchars( $m['msg'] );
-
-		if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI] ) ) {
-			$pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . 
-				htmlspecialchars( $key ) . '</span>' );
-		} else {
-			$pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" . 
-				htmlspecialchars( $key ) . '</span>' );
+		$dbr->freeResult( $res );
+		
+		wfProfileOut( __METHOD__ . "-db" );
+		
+		return true;
+	}
+	
+	/* This function normally does a database query to get the results; we need
+	 * to make a pretend result using a FakeResultWrapper.
+	 */
+	function reallyDoQuery( $offset , $limit , $descending ){
+		$mResult = new FakeResultWrapper( array() );
+		
+		if( !$this->messages ) $this->getAllMessages( $descending );
+		if( $this->talkPages === NULL ) $this->markCustomisedMessages();
+		
+		$count = 0;
+		foreach( $this->messages as $key => $value ){
+			if( $value['customised'] !== $this->custom &&
+			    ( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) &&
+				(( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false )
+			  ){
+				$mResult->result[] = array( 'am_title'      => $key,
+										    'am_actual'     => $value['actual'],
+										    'am_default'    => $value['default'],
+											'am_customised' => $value['customised'],
+									  );
+				unset( $this->messages[$key] );	// save a few bytes 
+				$count++;
+			}
+			if( $count == $limit ) break;
 		}
-		if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI_TALK] ) ) {
-			$talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
-		} else {
-			$talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
+		unset( $this->messages ); //no longer needed, free up some memory
+		return $mResult;
+	}
+	
+	function getStartBody() {
+		return "<table border=\"1\" class=\"TablePager\" style=\"width:100%;\" id=\"allmessagestable\"><thead>\n<tr>" . 
+			   "<th rowspan=\"2\">" . wfMsg('allmessagesname') . "</th><th>" . wfMsg('allmessagesdefault') . 
+			   "</tr>\n<tr><th>" . wfMsg('allmessagescurrent') . "</th></tr>\n";
+	}
+	
+	function formatValue( $field , $value ){
+		global $wgLang;
+		switch( $field ){
+			
+			case 'am_title' : 
+				
+				$title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix );
+				$talk  = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix );
+				
+				if( $this->mCurrentRow->am_customised ){
+					$title = $this->mSkin->makeKnownLinkObj(  $title, $wgLang->lcfirst( $value ) );
+				} else {
+					$title = $this->mSkin->makeBrokenLinkObj( $title, $wgLang->lcfirst( $value ) );
+				}
+				if( array_key_exists( $talk->getDBkey() , $this->talkPages ) ) {
+					$talk = $this->mSkin->makeKnownLinkObj( $talk , $this->talk );
+				} else {
+					$talk = $this->mSkin->makeBrokenLinkObj( $talk , $this->talk );
+				}
+				return $title . ' (' . $talk . ')';
+				
+			case 'am_default' :
+				return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES );
+			case 'am_actual' : 
+				return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES );
 		}
-
-		$anchor = 'msg_' . htmlspecialchars( strtolower( $title ) );
-		$anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>";
-
-		if( $changed ) {
-			$txt .= "
-				<tr class=\"orig\" id=\"sp-allmessages-r1-$i\">
-					<td rowspan=\"2\">
-						$anchor$pageLink<br />$talkLink
-					</td><td>
-				$message
-					</td>
-				</tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\">
-					<td>
-				$mw
-					</td>
-				</tr>";
+		return '';
+	}
+	
+	function formatRow( $row ){
+		//Do all the normal stuff
+		$s = parent::formatRow( $row );
+		
+		//But if there's a customised message, add that too.
+		if( $row->am_customised ){
+			$s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) );
+			$formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) );
+			if ( $formatted == '' ) {
+				$formatted = '&nbsp;';
+			}
+			$s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) 
+			   . "</tr>\n";
+		}
+		return $s;		
+	}
+	
+	function getRowAttrs( $row, $isSecond=false ){
+		$arr = array();
+		global $wgLang;
+		if( $row->am_customised ){
+			$arr['class'] =  'allmessages-customised';
+		} 
+		if( !$isSecond ){
+			$arr['id'] = Sanitizer::escapeId( 'msg_' . $wgLang->lcfirst( $row->am_title ) );
+		}
+		return $arr;
+	}
+	
+	function getCellAttrs( $field, $value ){
+		if( $this->mCurrentRow->am_customised && $field == 'am_title' ){
+			return array( 'rowspan' => '2', 'class' => $field );
 		} else {
-			$txt .= "
-				<tr class=\"def\" id=\"sp-allmessages-r1-$i\">
-					<td>
-						$anchor$pageLink<br />$talkLink
-					</td><td>
-				$mw
-					</td>
-				</tr>";
+			return array( 'class' => $field );
 		}
-		$messages[$key] = NULL; // trade bytes
-		$i++;
 	}
-	$txt .= '</table>';
-	wfProfileOut( __METHOD__ . '-output' );
+	
+	// This is not actually used, as getStartBody is overridden above
+	function getFieldNames() {
+		return array( 'am_title'   => wfMsg('allmessagesname'),
+					  'am_default' => wfMsg('allmessagesdefault') );
+	}
+	function getTitle() {
+		return SpecialPage::getTitleFor( 'Allmessages', false );
+	}
+	function isFieldSortable( $x ){
+		return false;
+	}
+	function getDefaultSort(){
+		return '';
+	}
+	function getQueryInfo(){
+		return '';
+	}
+}
+/* Overloads the relevant methods of the real ResultsWrapper so it
+ * doesn't go anywhere near an actual database.
+ */
+class FakeResultWrapper extends ResultWrapper {
+	
+	var $result     = array();
+	var $db         = NULL;		//And it's going to stay that way :D
+	var $pos        = 0;
+	var $currentRow = NULL;
+	
+	function __construct( $array ){
+		$this->result = $array;
+	}
+	
+	function numRows() {
+		return count( $this->result ); 
+	}
+	
+	function fetchRow() {
+		$this->currentRow = $this->result[$this->pos++];
+		return $this->currentRow;
+	}
+	
+	function seek( $row ) {
+		$this->pos = $row;
+	}
 
-	wfProfileOut( __METHOD__ );
-	return $txt;
+	function free() {}
+	
+	// Callers want to be able to access fields with $this->fieldName
+	function fetchObject(){
+		$this->currentRow = $this->result[$this->pos++];
+		return (object)$this->currentRow; 
+	}
+	
+	function rewind() {
+		$this->pos = 0;
+		$this->currentRow = NULL;
+	}
 }
Index: trunk/phase3/includes/SpecialPage.php
===================================================================
--- trunk/phase3/includes/SpecialPage.php	(revision 50159)
+++ trunk/phase3/includes/SpecialPage.php	(working copy)
@@ -145,7 +145,7 @@
 
 		# Wiki data and tools
 		'Statistics'				=> 'SpecialStatistics',
-		'Allmessages'               => array( 'SpecialPage', 'Allmessages' ),
+		'Allmessages'               => 'SpecialAllmessages',
 		'Version'                   => 'SpecialVersion',
 		'Lockdb'                    => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
 		'Unlockdb'                  => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
@@ -951,4 +951,4 @@
 		global $wgUser;
 		return SpecialPage::getTitleFor( 'Contributions', $wgUser->getName() );
 	}
-}
+}
\ No newline at end of file
Index: trunk/phase3/languages/messages/MessagesEn.php
===================================================================
--- trunk/phase3/languages/messages/MessagesEn.php	(revision 50159)
+++ trunk/phase3/languages/messages/MessagesEn.php	(working copy)
@@ -2837,15 +2837,18 @@
 'export-pagelinks'  => 'Include linked pages to a depth of:',
 
 # Namespace 8 related
-'allmessages'               => 'System messages',
-'allmessagesname'           => 'Name',
-'allmessagesdefault'        => 'Default text',
-'allmessagescurrent'        => 'Current text',
-'allmessagestext'           => 'This is a list of system messages available in the MediaWiki namespace.
+'allmessages'                   => 'System messages',
+'allmessagesname'               => 'Name',
+'allmessagesdefault'            => 'Default text',
+'allmessagescurrent'            => 'Current text',
+'allmessagestext'               => 'This is a list of system messages available in the MediaWiki namespace.
 Please visit [http://www.mediawiki.org/wiki/Localisation MediaWiki Localisation] and [http://translatewiki.net translatewiki.net] if you wish to contribute to the generic MediaWiki localisation.',
-'allmessagesnotsupportedDB' => "This page cannot be used because '''\$wgUseDatabaseMessages''' has been disabled.",
-'allmessagesfilter'         => 'Message name filter:',
-'allmessagesmodified'       => 'Show only modified',
+'allmessagesnotsupportedDB'     => "This page cannot be used because '''\$wgUseDatabaseMessages''' has been disabled.",
+'allmessages-filter'            => 'Filter by customisation state:',
+'allmessages-filter-unmodified' => 'Unmodified',
+'allmessages-filter-all'        => 'All',
+'allmessages-filter-modified'   => 'Modified',
+'allmessages-prefix'            => 'Filter by prefix:',
 
 # Thumbnails
 'thumbnail-more'           => 'Enlarge',
Index: trunk/phase3/RELEASE-NOTES
===================================================================
--- trunk/phase3/RELEASE-NOTES	(revision 50159)
+++ trunk/phase3/RELEASE-NOTES	(working copy)
@@ -47,6 +47,7 @@
   Special:Version
 * (bug 18420) Missing file revisions are handled gracefully now
 * (bug 9219) Auth plugins can control editing RealName/Email/Nick preferences
+* (bug 16497) Special:Allmessages is paginated
 
 === Bug fixes in 1.16 ===
 
