[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-commits
Subject:    [websites/wiki-kde-org/develop] extensions/LocalisationUpdate: add localisationupdate extension
From:       Ingo Malchow <imalchow () kde ! org>
Date:       2012-03-31 17:35:48
Message-ID: 20120331173548.AD23EA60A9 () git ! kde ! org
[Download RAW message or body]

Git commit 24d8d12117dcce6b7e730064f5c2e6f0cc072f0d by Ingo Malchow.
Committed on 31/03/2012 at 19:35.
Pushed by imalchow into branch 'develop'.

add localisationupdate extension

A  +11   -0    extensions/LocalisationUpdate/KNOWN_ISSUES.txt
A  +748  -0    extensions/LocalisationUpdate/LocalisationUpdate.class.php
A  +506  -0    extensions/LocalisationUpdate/LocalisationUpdate.i18n.php
A  +51   -0    extensions/LocalisationUpdate/LocalisationUpdate.php
A  +187  -0    extensions/LocalisationUpdate/QuickArrayReader.php
A  +8    -0    extensions/LocalisationUpdate/README_FIRST.txt
A  +91   -0    extensions/LocalisationUpdate/tests/tokenTest.php
A  +38   -0    extensions/LocalisationUpdate/update.php

http://commits.kde.org/websites/wiki-kde-org/24d8d12117dcce6b7e730064f5c2e6f0cc072f0d

diff --git a/extensions/LocalisationUpdate/KNOWN_ISSUES.txt \
b/extensions/LocalisationUpdate/KNOWN_ISSUES.txt new file mode 100644
index 0000000..7ce14cd
--- /dev/null
+++ b/extensions/LocalisationUpdate/KNOWN_ISSUES.txt
@@ -0,0 +1,11 @@
+- Only works with SVN revision 50605 or later of the
+  MediaWiki core
+
+
+
+Key issues at the moment:
+* Seems to want to store a copy of the localization updates in each local database.
+We've got hundreds of wikis run from the same installation set; we don't want to \
multiply our effort by 1000. +
+* It doesn't seem to be using available memcached stuff; unsure yet whether this is \
taken care of +by the general message caching or if we're going to end up making \
                extra hits we don't need.
diff --git a/extensions/LocalisationUpdate/LocalisationUpdate.class.php \
b/extensions/LocalisationUpdate/LocalisationUpdate.class.php new file mode 100644
index 0000000..cf0e6e6
--- /dev/null
+++ b/extensions/LocalisationUpdate/LocalisationUpdate.class.php
@@ -0,0 +1,748 @@
+<?php
+
+/**
+ * Class for localization updates.
+ *
+ * TODO: refactor code to remove duplication
+ */
+class LocalisationUpdate {
+
+	private static $newHashes = null;
+	private static $filecache = array();
+
+	/**
+	 * LocalisationCacheRecache hook handler.
+	 *
+	 * @param $lc LocalisationCache
+	 * @param $langcode String
+	 * @param $cache Array
+	 *
+	 * @return true
+	 */
+	public static function onRecache( LocalisationCache $lc, $langcode, array &$cache ) \
{ +		// Handle fallback sequence and load all fallback messages from the cache
+		$codeSequence = array_merge( array( $langcode ), $cache['fallbackSequence'] );
+		// Iterate over the fallback sequence in reverse, otherwise the fallback
+		// language will override the requested language
+		foreach ( array_reverse( $codeSequence ) as $code ) {
+			if ( $code == 'en' ) {
+				// Skip English, otherwise we end up trying to read
+				// the nonexistent cache file for en a couple hundred times
+				continue;
+			}
+
+			$cache['messages'] = array_merge(
+				$cache['messages'],
+				self::readFile( $code )
+			);
+
+			$cache['deps'][] = new FileDependency(
+				self::filename( $code )
+			);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Called from the cronjob to fetch new messages from SVN.
+	 *
+	 * @param $options Array
+	 *
+	 * @return true
+	 */
+	public static function updateMessages( array $options ) {
+		global $wgLocalisationUpdateDirectory, $wgLocalisationUpdateSVNURL;
+
+		$verbose = !isset( $options['quiet'] );
+		$all = isset( $options['all'] );
+		$skipCore = isset( $options['skip-core'] );
+		$skipExtensions = isset( $options['skip-extensions'] );
+
+		if( isset( $options['outdir'] ) ) {
+			$wgLocalisationUpdateDirectory = $options['outdir'];
+		}
+		
+		if ( isset( $options['svnurl'] ) ) {
+			// FIXME: Ewwwww. Refactor so this can be done properly
+			$wgLocalisationUpdateSVNURL = $options['svnurl'];
+		}
+
+		$result = 0;
+
+		// Update all MW core messages.
+		if( !$skipCore ) {
+			$result = self::updateMediawikiMessages( $verbose );
+		}
+
+		// Update all Extension messages.
+		if( !$skipExtensions ) {
+			if( $all ) {
+				global $IP;
+				$extFiles = array();
+
+				// Look in extensions/ for all available items...
+				// TODO: add support for $wgExtensionAssetsPath
+				$dirs = new RecursiveDirectoryIterator( "$IP/extensions/" );
+
+				// I ain't kidding... RecursiveIteratorIterator.
+				foreach( new RecursiveIteratorIterator( $dirs ) as $pathname => $item ) {
+					$filename = basename( $pathname );
+					$matches = array();
+					if( preg_match( '/^(.*)\.i18n\.php$/', $filename, $matches ) ) {
+						$group = $matches[1];
+						$extFiles[$group] = $pathname;
+					}
+				}
+			} else {
+				global $wgExtensionMessagesFiles;
+				$extFiles = $wgExtensionMessagesFiles;
+			}
+			foreach ( $extFiles as $extension => $locFile ) {
+				$result += self::updateExtensionMessages( $locFile, $extension, $verbose );
+			}
+		}
+
+		self::writeHashes();
+
+		// And output the result!
+		self::myLog( "Updated {$result} messages in total" );
+		self::myLog( "Done" );
+
+		return true;
+	}
+
+	/**
+	 * Update Extension Messages.
+	 *
+	 * @param $file String
+	 * @param $extension String
+	 * @param $verbose Boolean
+	 *
+	 * @return Integer: the amount of updated messages
+	 */
+	public static function updateExtensionMessages( $file, $extension, $verbose ) {
+		global $IP, $wgLocalisationUpdateSVNURL;
+
+		$relfile = wfRelativePath( $file, "$IP/extensions" );
+
+		// Create a full path.
+		// TODO: add support for $wgExtensionAssetsPath
+		// $localfile = "$IP/extensions/$relfile";
+
+		// Get the full SVN directory path.
+		// TODO: add support for $wgExtensionAssetsPath
+		$svnfile = "$wgLocalisationUpdateSVNURL/extensions/$relfile";
+
+		// Compare the 2 files.
+		$result = self::compareExtensionFiles( $extension, $svnfile, $file, $verbose, \
false, true ); +
+		return $result;
+	}
+
+	/**
+	 * Update the MediaWiki Core Messages.
+	 *
+	 * @param $verbose Boolean
+	 *
+	 * @return Integer: the amount of updated messages
+	 */
+	public static function updateMediawikiMessages( $verbose ) {
+		global $IP, $wgLocalisationUpdateSVNURL;
+
+		// Create an array which will later contain all the files that we want to try to \
update. +		$files = array();
+
+		// The directory which contains the files.
+		$dirname = "languages/messages";
+
+		// Get the full path to the directory.
+		$localdir = $IP . "/" . $dirname;
+
+		// Get the full SVN Path.
+		$svndir = "$wgLocalisationUpdateSVNURL/phase3/$dirname";
+
+		// Open the directory.
+		$dir = opendir( $localdir );
+		while ( false !== ( $file = readdir( $dir ) ) ) {
+			$m = array();
+
+			// And save all the filenames of files containing messages
+			if ( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $file, $m ) ) {
+				if ( $m[1] != 'En' ) { // Except for the English one.
+					$files[] = $file;
+				}
+			}
+		}
+		closedir( $dir );
+
+		// Find the changed English strings (as these messages won't be updated in ANY \
language). +		$changedEnglishStrings = self::compareFiles( $localdir . \
'/MessagesEn.php', $svndir . '/MessagesEn.php', $verbose ); +
+		// Count the changes.
+		$changedCount = 0;
+
+		// For each language.
+		sort( $files );
+		foreach ( $files as $file ) {
+			$svnfile = $svndir . '/' . $file;
+			$localfile = $localdir . '/' . $file;
+
+			// Compare the files.
+			$result = self::compareFiles( $svnfile, $localfile, $verbose, \
$changedEnglishStrings, false, true ); +
+			// And update the change counter.
+			$changedCount += count( $result );
+		}
+
+		// Log some nice info.
+		self::myLog( "{$changedCount} MediaWiki messages are updated" );
+
+		return $changedCount;
+	}
+
+	/**
+	 * Removes all unneeded content from a file and returns it.
+	 *
+	 * @param $contents String
+	 *
+	 * @return String
+	 */
+	public static function cleanupFile( $contents ) {
+		// We don't need any PHP tags.
+		$contents = strtr( $contents,
+			array(
+				'<?php' => '',
+				'?' . '>' => ''
+			)
+		);
+
+		$results = array();
+
+		// And we only want the messages array.
+		preg_match( "/\\\$messages(.*\s)*?\);/", $contents, $results );
+
+		// If there is any!
+		if ( !empty( $results[0] ) ) {
+			$contents = $results[0];
+		} else {
+			$contents = '';
+		}
+
+		// Windows vs Unix always stinks when comparing files.
+		$contents = preg_replace( "/\\r\\n?/", "\n", $contents );
+
+		// Return the cleaned up file.
+		return $contents;
+	}
+
+	/**
+	 * Removes all unneeded content from a file and returns it.
+	 *
+	 * FIXME: duplicated cleanupFile code
+	 *
+	 * @param $contents String
+	 *
+	 * @return string
+	 */
+	public static function cleanupExtensionFile( $contents ) {
+		// We don't want PHP tags.
+		$contents = preg_replace( "/<\?php/", "", $contents );
+		$contents = preg_replace( "/\?" . ">/", "", $contents );
+
+		$results = array();
+
+		// And we only want message arrays.
+		preg_match_all( "/\\\$messages(.*\s)*?\);/", $contents, $results );
+
+		// But we want them all in one string.
+		if( !empty( $results[0] ) && is_array( $results[0] ) ) {
+			$contents = implode( "\n\n", $results[0] );
+		} else {
+			$contents = '';
+		}
+
+		// And we hate the windows vs linux linebreaks.
+		$contents = preg_replace( "/\\\r\\\n?/", "\n", $contents );
+
+		return $contents;
+	}
+
+	/**
+	 * Returns the contents of a file or false on failiure.
+	 *
+	 * @param $basefile String
+	 *
+	 * @return string or false
+	 */
+	public static function getFileContents( $basefile ) {
+		global $wgLocalisationUpdateRetryAttempts;
+
+		$attempts = 0;
+		$basefilecontents = '';
+
+		// Use cURL to get the SVN contents.
+		if ( preg_match( "/^http/", $basefile ) ) {
+			while( !$basefilecontents && $attempts <= $wgLocalisationUpdateRetryAttempts ) {
+				if( $attempts > 0 ) {
+					$delay = 1;
+					self::myLog( 'Failed to download ' . $basefile . "; retrying in ${delay}s..." \
); +					sleep( $delay );
+				}
+
+				$basefilecontents = Http::get( $basefile );
+				$attempts++;
+			}
+			if ( !$basefilecontents ) {
+				self::myLog( 'Cannot get the contents of ' . $basefile . ' (curl)' );
+				return false;
+			}
+		} else {// otherwise try file_get_contents
+			if ( !( $basefilecontents = file_get_contents( $basefile ) ) ) {
+				self::myLog( 'Cannot get the contents of ' . $basefile );
+				return false;
+			}
+		}
+
+		return $basefilecontents;
+	}
+
+	/**
+	 * Returns an array containing the differences between the files.
+	 *
+	 * @param $basefile String
+	 * @param $comparefile String
+	 * @param $verbose Boolean
+	 * @param $forbiddenKeys Array
+	 * @param $alwaysGetResult Boolean
+	 * @param $saveResults Boolean
+	 *
+	 * @return array
+	 */
+	public static function compareFiles( $basefile, $comparefile, $verbose, array \
$forbiddenKeys = array(), $alwaysGetResult = true, $saveResults = false ) { +		// Get \
the languagecode. +		$langcode = Language::getCodeFromFileName( $basefile, 'Messages' \
); +
+		$basefilecontents = self::getFileContents( $basefile );
+
+		if ( $basefilecontents === false || $basefilecontents === '' ) {
+			self::myLog( "Failed to read $basefile" );
+			return array();
+		}
+
+		// Only get the part we need.
+		$basefilecontents = self::cleanupFile( $basefilecontents );
+
+		// Change the variable name.
+		$basefilecontents = preg_replace( "/\\\$messages/", "\$base_messages", \
$basefilecontents ); +		$basehash = md5( $basefilecontents );
+
+		// Check if the file has changed since our last update.
+		if ( !$alwaysGetResult ) {
+			if ( !self::checkHash( $basefile, $basehash ) ) {
+				self::myLog( "Skipping {$langcode} since the remote file hasn't changed since \
our last update", $verbose ); +				return array();
+			}
+		}
+
+		// Get the array with messages.
+		$base_messages = self::parsePHP( $basefilecontents, 'base_messages' );
+		if ( !is_array( $base_messages ) ) {
+			if ( strpos( $basefilecontents, "\$base_messages" ) === false ) {
+				// No $messages array. This happens for some languages that only have a fallback
+				$base_messages = array();
+			} else {
+				// Broken file? Report and bail
+				self::myLog( "Failed to parse $basefile" );
+				return array();
+			}
+		}
+
+		$comparefilecontents = self::getFileContents( $comparefile );
+
+		if ( $comparefilecontents === false || $comparefilecontents === '' ) {
+			self::myLog( "Failed to read $comparefile" );
+			return array();
+		}
+
+		// Only get the stuff we need.
+		$comparefilecontents = self::cleanupFile( $comparefilecontents );
+
+		// Rename the array.
+		$comparefilecontents = preg_replace( "/\\\$messages/", "\$compare_messages", \
$comparefilecontents ); +		$comparehash = md5( $comparefilecontents );
+
+		// If this is the remote file check if the file has changed since our last update.
+		if ( preg_match( "/^http/", $comparefile ) && !$alwaysGetResult ) {
+			if ( !self::checkHash( $comparefile, $comparehash ) ) {
+				self::myLog( "Skipping {$langcode} since the remote file has not changed since \
our last update", $verbose ); +				return array();
+			}
+		}
+
+		// Get the array.
+		$compare_messages = self::parsePHP( $comparefilecontents, 'compare_messages' );
+		if ( !is_array( $compare_messages ) ) {
+			// Broken file? Report and bail
+			if ( strpos( $comparefilecontents, "\$compare_messages" ) === false ) {
+				// No $messages array. This happens for some languages that only have a fallback
+				self::myLog( "Skipping $langcode , no messages array in $comparefile", $verbose \
); +				$compare_messages = array();
+			} else {
+				self::myLog( "Failed to parse $comparefile" );
+				return array();
+			}
+		}
+
+		// If the localfile and the remote file are the same, skip them!
+		if ( $basehash == $comparehash && !$alwaysGetResult ) {
+			self::myLog( "Skipping {$langcode} since the remote file is the same as the local \
file", $verbose ); +			return array();
+		}
+
+		// Add the messages we got with our previous update(s) to the local array (as we \
already got these as well). +		$compare_messages = array_merge(
+			$compare_messages,
+			self::readFile( $langcode )
+		);
+
+		// Compare the remote and local message arrays.
+		$changedStrings = array_diff_assoc( $base_messages, $compare_messages );
+
+		// If we want to save the differences.
+		// HACK: because of the hack in saveChanges(), we need to call that function
+		// even if $changedStrings is empty. So comment out the $changedStrings checks \
below. +		if ( $saveResults /* && !empty( $changedStrings ) && is_array( \
$changedStrings )*/ ) { +			self::myLog( "--Checking languagecode {$langcode}--", \
$verbose ); +			// Save the differences.
+			$updates = self::saveChanges( $changedStrings, $forbiddenKeys, $compare_messages, \
$base_messages, $langcode, $verbose ); +			self::myLog( "{$updates} messages updated \
for {$langcode}.", $verbose ); +		} /*elseif ( $saveResults ) {
+			self::myLog( "--{$langcode} hasn't changed--", $verbose );
+		}*/
+
+		self::saveHash( $basefile, $basehash );
+
+		self::saveHash( $comparefile, $comparehash );
+
+		return $changedStrings;
+	}
+
+	/**
+	 * Checks whether a messages file has a certain hash.
+	 *
+	 * TODO: Swap return values, this is insane
+	 *
+	 * @param $file string Filename
+	 * @param $hash string Hash
+	 *
+	 * @return bool True if $file does NOT have hash $hash, false if it does
+	 */
+	public static function checkHash( $file, $hash ) {
+		$hashes = self::readFile( 'hashes' );
+		return @$hashes[$file] !== $hash;
+	}
+
+	/**
+	 * @param $file
+	 * @param $hash
+	 */
+	public static function saveHash( $file, $hash ) {
+		if ( is_null( self::$newHashes ) ) {
+			self::$newHashes = self::readFile( 'hashes' );
+		}
+
+		self::$newHashes[$file] = $hash;
+	}
+
+	public static function writeHashes() {
+		self::writeFile( 'hashes', self::$newHashes );
+	}
+
+	/**
+	 *
+	 *
+	 * @param $changedStrings Array
+	 * @param $forbiddenKeys Array
+	 * @param $compare_messages Array
+	 * @param $base_messages Array
+	 * @param $langcode String
+	 * @param $verbose Boolean
+	 *
+	 * @return Integer: the amount of updated messages
+	 */
+	public static function saveChanges( $changedStrings, array $forbiddenKeys, array \
$compare_messages, array $base_messages, $langcode, $verbose ) { +		// Count the \
updates. +		$updates = 0;
+
+		if( !is_array( $changedStrings ) ) {
+			self::myLog("CRITICAL ERROR: \$changedStrings is not an array in file " . \
(__FILE__) . ' at line ' .( __LINE__ ) ); +			return 0;
+		}
+
+		// This function is run once for core and once for each extension,
+		// so make sure messages from previous runs aren't lost
+		$new_messages = self::readFile( $langcode );
+
+		//foreach ( $changedStrings as $key => $value ) {
+		// HACK for r103763 CR: store all messages, even unchanged ones
+		// TODO this file is a mess and needs to be rewritten
+		foreach ( array_merge( array_keys( $base_messages ), array_keys( $compare_messages \
) ) as $key ) { +			// Only update the translation if this message wasn't changed in \
English +			if ( !isset( $forbiddenKeys[$key] ) && isset( $base_messages[$key] ) ) {
+				$new_messages[$key] = $base_messages[$key];
+
+				if ( !isset( $compare_messages[$key] ) || $compare_messages[$key] !== \
$base_messages[$key] ) { +					// Output extra logmessages when needed.
+					if ( $verbose ) {
+						$oldmsg = isset( $compare_messages[$key] ) ? "'{$compare_messages[$key]}'" : \
'not set'; +						self::myLog( "Updated message {$key} from $oldmsg to \
'{$base_messages[$key]}'", $verbose ); +					}
+
+					// Update the counter.
+					$updates++;
+				}
+			} elseif ( isset( $forbiddenKeys[$key] ) && isset( $compare_messages[$key] ) ) {
+				// The message was changed in English, but a previous translation still exists \
in the cache. +				// Use that previous translation rather than falling back to the \
.i18n.php file +				$new_messages[$key] = $compare_messages[$key];
+			}
+			// Other possible cases:
+			// * The messages is no longer in the SVN file, but is present in the local i18n \
file or in the cache +			// * The message was changed in English, and there is no \
previous translation in the i18n file or in the cache +			// In both cases, we can \
safely do nothing +		}
+		self::writeFile( $langcode, $new_messages );
+
+		return $updates;
+	}
+
+	/**
+	 *
+	 * @param $extension String
+	 * @param $basefile String
+	 * @param $comparefile String
+	 * @param $verbose Boolean
+	 * @param $alwaysGetResult Boolean
+	 * @param $saveResults Boolean
+	 *
+	 * @return Integer: the amount of updated messages
+	 */
+	public static function compareExtensionFiles( $extension, $basefile, $comparefile, \
$verbose, $alwaysGetResult = true, $saveResults = false ) { +		// FIXME: Factor out \
duplicated code? +
+		$basefilecontents = self::getFileContents( $basefile );
+
+		if ( $basefilecontents === false || $basefilecontents === '' ) {
+			return 0; // Failed
+		}
+
+		// Cleanup the file where needed.
+		$basefilecontents = self::cleanupExtensionFile( $basefilecontents );
+
+		// Rename the arrays.
+		$basefilecontents = preg_replace( "/\\\$messages/", "\$base_messages", \
$basefilecontents ); +		$basehash = md5( $basefilecontents );
+
+		// If this is the remote file
+		if ( preg_match( "/^http/", $basefile ) && !$alwaysGetResult ) {
+			// Check if the hash has changed
+			if ( !self::checkHash( $basefile, $basehash ) ) {
+				self::myLog( "Skipping {$extension} since the remote file has not changed since \
our last update", $verbose ); +				return 0;
+			}
+		}
+
+		// And get the real contents
+		$base_messages = self::parsePHP( $basefilecontents, 'base_messages' );
+
+		$comparefilecontents = self::getFileContents( $comparefile );
+
+		if ( $comparefilecontents === false || $comparefilecontents === '' ) {
+			return 0; // Failed
+		}
+
+		// Only get what we need.
+		$comparefilecontents = self::cleanupExtensionFile( $comparefilecontents );
+
+		// Rename the array.
+		$comparefilecontents = preg_replace( "/\\\$messages/", "\$compare_messages", \
$comparefilecontents ); +		$comparehash = md5( $comparefilecontents );
+
+		if ( preg_match( "/^http/", $comparefile ) && !$alwaysGetResult ) {
+			// Check if the remote file has changed
+			if ( !self::checkHash( $comparefile, $comparehash ) ) {
+				self::myLog( "Skipping {$extension} since the remote file has not changed since \
our last update", $verbose ); +				return 0;
+			}
+		}
+
+		// Get the real array.
+		$compare_messages = self::parsePHP( $comparefilecontents, 'compare_messages' );
+
+		// If both files are the same, they can be skipped.
+		if ( $basehash == $comparehash && !$alwaysGetResult ) {
+			self::myLog( "Skipping {$extension} since the remote file is the same as the \
local file", $verbose ); +			return 0;
+		}
+
+		// Update counter.
+		$updates = 0;
+
+		if ( !is_array( $base_messages ) ) {
+			$base_messages = array();
+		}
+
+		if ( empty( $base_messages['en'] ) ) {
+			$base_messages['en'] = array();
+		}
+
+		if ( !is_array( $compare_messages ) ) {
+			$compare_messages = array();
+		}
+
+		if ( empty( $compare_messages['en'] ) ) {
+			$compare_messages['en'] = array();
+		}
+
+		// Find the changed english strings.
+		$forbiddenKeys = array_diff_assoc( $base_messages['en'], $compare_messages['en'] \
); +
+		// Do an update for each language.
+		foreach ( $base_messages as $language => $messages ) {
+			if ( $language == 'en' ) { // Skip english.
+				continue;
+			}
+
+			if ( !isset( $compare_messages[$language] ) ) {
+				$compare_messages[$language] = array();
+			}
+			// Add the already known messages to the array so we will only find new changes.
+			$compare_messages[$language] = array_merge(
+				$compare_messages[$language],
+				self::readFile( $language )
+			);
+
+			if ( empty( $compare_messages[$language] ) || !is_array( \
$compare_messages[$language] ) ) { +				$compare_messages[$language] = array();
+			}
+
+			// Get the array of changed strings.
+			$changedStrings = array_diff_assoc( $messages, $compare_messages[$language] );
+
+			// If we want to save the changes.
+			// HACK: because of the hack in saveChanges(), we need to call that function
+			// even if $changedStrings is empty. So comment out the $changedStrings checks \
below. +			if ( $saveResults === true /*&& !empty( $changedStrings ) && is_array( \
$changedStrings )*/ ) { +				self::myLog( "--Checking languagecode {$language}--", \
$verbose ); +				// The save them
+				$updates = self::saveChanges( $changedStrings, $forbiddenKeys, \
$compare_messages[$language], $messages, $language, $verbose ); +				self::myLog( \
"{$updates} messages updated for {$language}.", $verbose ); +			}/* \
elseif($saveResults === true) { +				self::myLog( "--{$language} hasn't changed--", \
$verbose ); +			}*/
+		}
+
+		// And log some stuff.
+		self::myLog( "Updated " . $updates . " messages for the '{$extension}' extension", \
$verbose ); +
+		self::saveHash( $basefile, $basehash );
+
+		self::saveHash( $comparefile, $comparehash );
+
+		return $updates;
+	}
+
+	/**
+	 * Logs a message.
+	 *
+	 * @param $log String
+	 * @param bool $verbose
+	 */
+	public static function myLog( $log, $verbose = true ) {
+		if ( !$verbose ) {
+			return;
+		}
+		if ( isset( $_SERVER ) && array_key_exists( 'REQUEST_METHOD', $_SERVER ) ) {
+			wfDebug( $log . "\n" );
+		} else {
+			print( $log . "\n" );
+		}
+	}
+
+	/**
+	 * @param $php
+	 * @param $varname
+	 * @return bool|array
+	 */
+	public static function parsePHP( $php, $varname ) {
+		try {
+			$reader = new QuickArrayReader("<?php $php");
+			return $reader->getVar( $varname );
+		} catch( Exception $e ) {
+			self::myLog( "Failed to read file: " . $e );
+			return false;
+		}
+	}
+
+	/**
+	 * @param $lang
+	 * @return string
+	 * @throws MWException
+	 */
+	public static function filename( $lang ) {
+		global $wgLocalisationUpdateDirectory, $wgCacheDirectory;
+
+		$dir = $wgLocalisationUpdateDirectory ?
+			$wgLocalisationUpdateDirectory :
+			$wgCacheDirectory;
+
+		if ( !$dir ) {
+			throw new MWException( 'No cache directory configured' );
+		}
+
+		return "$dir/l10nupdate-$lang.cache";
+	}
+
+	/**
+	 * @param $lang
+	 * @return mixed
+	 */
+	public static function readFile( $lang ) {
+		if ( !isset( self::$filecache[$lang] ) ) {
+			$file = self::filename( $lang );
+			$contents = @file_get_contents( $file );
+
+			if ( $contents === false ) {
+				wfDebug( "Failed to read file '$file'\n" );
+				$retval = array();
+			} else {
+				$retval = unserialize( $contents );
+
+				if ( $retval === false ) {
+					wfDebug( "Corrupted data in file '$file'\n" );
+					$retval = array();
+				}
+			}
+			self::$filecache[$lang] = $retval;
+		}
+
+		return self::$filecache[$lang];
+	}
+
+	/**
+	 * @param $lang
+	 * @param $var
+	 * @throws MWException
+	 */
+	public static function writeFile( $lang, $var ) {
+		$file = self::filename( $lang );
+
+		if ( !@file_put_contents( $file, serialize( $var ) ) ) {
+			throw new MWException( "Failed to write to file '$file'" );
+		}
+
+		self::$filecache[$lang] = $var;
+	}
+
+}
diff --git a/extensions/LocalisationUpdate/LocalisationUpdate.i18n.php \
b/extensions/LocalisationUpdate/LocalisationUpdate.i18n.php new file mode 100644
index 0000000..75c3e5d
--- /dev/null
+++ b/extensions/LocalisationUpdate/LocalisationUpdate.i18n.php
@@ -0,0 +1,506 @@
+<?php
+/**
+ * Internationalisation file for LocalisationUpdate extension.
+ *
+ * @file
+ * @ingroup Extensions
+ */
+ 
+$messages = array();
+
+/** English
+ * @author Tom Maaswinkel
+ */
+$messages['en'] = array(
+	'localisationupdate-desc' => 'Keeps the localised messages as up to date as \
possible', +);
+
+/** Message documentation (Message documentation)
+ * @author Fryed-peach
+ * @author Purodha
+ */
+$messages['qqq'] = array(
+	'localisationupdate-desc' => '{{desc}}',
+);
+
+/** Afrikaans (Afrikaans)
+ * @author Naudefj
+ */
+$messages['af'] = array(
+	'localisationupdate-desc' => 'Hou die gelokaliseerde boodskappe so op datum as \
moontlik', +);
+
+/** Arabic (العربية)
+ * @author Meno25
+ */
+$messages['ar'] = array(
+	'localisationupdate-desc' => 'يبقي الرسائل المترجمة محدثة \
كأفضل ما يكون', +);
+
+/** Asturian (Asturianu)
+ * @author Xuacu
+ */
+$messages['ast'] = array(
+	'localisationupdate-desc' => 'Caltién los mensaxes llocalizaos tan anovaos como se \
pueda', +);
+
+/** Bashkir (Башҡортса)
+ * @author Assele
+ */
+$messages['ba'] = array(
+	'localisationupdate-desc' => 'Локалләштерелгән \
хәбәрҙәрҙең мөмкин тиклем яңы булыуын тәьмин \
итә', +);
+
+/** Bavarian (Boarisch)
+ * @author Man77
+ */
+$messages['bar'] = array(
+	'localisationupdate-desc' => "Lokalisiade Texte und Nåchrichtn so aktuell håidn \
wia's gråd gehd", +);
+
+/** Belarusian (Taraškievica orthography) (‪Беларуская \
(тарашкевіца)‬) + * @author EugeneZelenko
+ * @author Wizardist
+ */
+$messages['be-tarask'] = array(
+	'localisationupdate-desc' => 'Сочыць за актуальнасьцю \
лякалізаваных паведамленьняў, наколькі гэта \
магчыма', +);
+
+/** Bulgarian (Български)
+ * @author DCLXVI
+ */
+$messages['bg'] = array(
+	'localisationupdate-desc' => 'Поддържа локализираните \
съобщения възможно най-актуални', +);
+
+/** Bengali (বাংলা)
+ * @author Bellayet
+ */
+$messages['bn'] = array(
+	'localisationupdate-desc' => 'স্থানীয়করণকৃত \
বার্তাসমূহ যথাসম্ভব হালনাগাদ \
রাখে', +);
+
+/** Breton (Brezhoneg)
+ * @author Fulup
+ */
+$messages['br'] = array(
+	'localisationupdate-desc' => "Derc'hel da hizivaat ar c'hemennoù troet ken fonnus \
ha ma'z eus tu", +);
+
+/** Bosnian (Bosanski)
+ * @author CERminator
+ */
+$messages['bs'] = array(
+	'localisationupdate-desc' => 'Zadržavanje lokaliziranih poruka ažurnim koliko je \
god moguće', +);
+
+/** Catalan (Catal )
+ * @author Paucabot
+ */
+$messages['ca'] = array(
+	'localisationupdate-desc' => 'Manté els missatges localitzats tan actualitzats com \
sigui possible', +);
+
+/** Czech (Česky)
+ * @author Mormegil
+ */
+$messages['cs'] = array(
+	'localisationupdate-desc' => 'Udržuje lokalizovaná hlášení co možná \
nejaktuálnější', +);
+
+/** Welsh (Cymraeg)
+ * @author Lloffiwr
+ */
+$messages['cy'] = array(
+	'localisationupdate-desc' => "Yn diweddaru'r cyfieithiadau o negeseuon mor aml â \
phosib", +);
+
+/** Danish (Dansk)
+ * @author Peter Alberti
+ */
+$messages['da'] = array(
+	'localisationupdate-desc' => 'Holder de lokaliserede meddelelser så opdaterede som \
muligt', +);
+
+/** German (Deutsch)
+ * @author Kghbln
+ * @author Purodha
+ */
+$messages['de'] = array(
+	'localisationupdate-desc' => 'Ermöglicht es lokalisierte Texte und Nachrichten so \
aktuell wie möglich zu halten', +);
+
+/** Lower Sorbian (Dolnoserbski)
+ * @author Michawiki
+ */
+$messages['dsb'] = array(
+	'localisationupdate-desc' => 'Źaržy lokalizěrowane powěźeńki tak aktualne ako \
móžno', +);
+
+/** Greek (Ελληνικά)
+ * @author Omnipaedista
+ */
+$messages['el'] = array(
+	'localisationupdate-desc' => 'Διατηρεί τις μεταφράσεις \
μηνυμάτων όσο πιο ενημερωμένες γίνεται', +);
+
+/** Esperanto (Esperanto)
+ * @author Yekrats
+ */
+$messages['eo'] = array(
+	'localisationupdate-desc' => 'Ĝisdatigas la asimilitajn mesaĝojn tiom eble',
+);
+
+/** Spanish (Español)
+ * @author Crazymadlover
+ */
+$messages['es'] = array(
+	'localisationupdate-desc' => 'Mantiene los mensajes localizados tan actualizados \
como sea posible', +);
+
+/** Estonian (Eesti)
+ * @author Pikne
+ */
+$messages['et'] = array(
+	'localisationupdate-desc' => 'Hoiab lokaliseeritud sõnumid nii ajakohased kui \
võimalik.', +);
+
+/** Basque (Euskara)
+ * @author Kobazulo
+ */
+$messages['eu'] = array(
+	'localisationupdate-desc' => 'Itzulitako mezuak ahalik eta eguneratuen mantentzen \
ditu', +);
+
+/** Persian (فارسی)
+ * @author ZxxZxxZ
+ */
+$messages['fa'] = array(
+	'localisationupdate-desc' => 'پیغام‌های محلی‌سازی‌شده را \
تا جای ممکن به‌روز نگه می‌دارد', +);
+
+/** Finnish (Suomi)
+ * @author Crt
+ * @author Nike
+ */
+$messages['fi'] = array(
+	'localisationupdate-desc' => 'Pitää ohjelmiston käännöksen ajantasaisena.',
+);
+
+/** French (Français)
+ * @author Crochet.david
+ */
+$messages['fr'] = array(
+	'localisationupdate-desc' => 'Maintenir la traduction des messages   jour autant \
que possible', +);
+
+/** Galician (Galego)
+ * @author Toliño
+ */
+$messages['gl'] = array(
+	'localisationupdate-desc' => 'Mantén as mensaxes localizadas tan actualizadas como \
é posible', +);
+
+/** Swiss German (Alemannisch)
+ * @author Als-Holder
+ */
+$messages['gsw'] = array(
+	'localisationupdate-desc' => 'Halt d Syschtemnochrichte so aktuälle wie megli',
+);
+
+/** Hebrew (עברית)
+ * @author YaronSh
+ */
+$messages['he'] = array(
+	'localisationupdate-desc' => 'שמירת ההודעות המתורגמות \
מעודכ ות ככל ה יתן', +);
+
+/** Hiligaynon (Ilonggo)
+ * @author Tagimata
+ */
+$messages['hil'] = array(
+	'localisationupdate-desc' => 'Gatugo sang mga mensahe nga lokal para mapahibalo \
sang madali', +);
+
+/** Croatian (Hrvatski)
+ * @author SpeedyGonsales
+ */
+$messages['hr'] = array(
+	'localisationupdate-desc' => 'Dogradnja za osvježavanje lokalizacije poruka \
MediaWikija', +);
+
+/** Upper Sorbian (Hornjoserbsce)
+ * @author Michawiki
+ */
+$messages['hsb'] = array(
+	'localisationupdate-desc' => 'Dźerži lokalizowane zdźělenki tak aktualne kaž \
móžno', +);
+
+/** Hungarian (Magyar)
+ * @author Glanthor Reviol
+ */
+$messages['hu'] = array(
+	'localisationupdate-desc' => 'Frissíti a lefordított üzeneteket',
+);
+
+/** Interlingua (Interlingua)
+ * @author McDutchie
+ */
+$messages['ia'] = array(
+	'localisationupdate-desc' => 'Mantene le messages localisate tanto actual como \
possibile', +);
+
+/** Indonesian (Bahasa Indonesia)
+ * @author Bennylin
+ */
+$messages['id'] = array(
+	'localisationupdate-desc' => 'Mengusahakan agar pesan-pesan yang telah \
diterjemahkan tetap semutakhir mungkin', +);
+
+/** Italian (Italiano)
+ * @author Darth Kule
+ */
+$messages['it'] = array(
+	'localisationupdate-desc' => 'Mantiene i messaggi localizzati quanto più \
aggiornati è possibile', +);
+
+/** Japanese (日本語)
+ * @author Fryed-peach
+ */
+$messages['ja'] = array(
+	'localisationupdate-desc' => \
'メッセージの翻訳を可能な限り最新に保つようにする', +);
+
+/** Khmer (ភាសាខ្មែរ)
+ * @author វ័ណថារិទ្ធ
+ */
+$messages['km'] = array(
+	'localisationupdate-desc' => \
'រក្សា​សារ​ដែលបាន​ប្រែសម្រួល​ទាំងឡាយ \
អោយនៅ​ទាន់សម័យ​តាមដែលអាចធ្វើទៅបាន​',
 +);
+
+/** Korean (한국어)
+ * @author Kwj2772
+ */
+$messages['ko'] = array(
+	'localisationupdate-desc' => '번역된 시스템 메시지를 가능한 한 최 \
으로  지', +);
+
+/** Colognian (Ripoarisch)
+ * @author Purodha
+ */
+$messages['ksh'] = array(
+	'localisationupdate-desc' => 'Texte un Nohreeschte vum Wiki esu joot wi müjjelich \
om neueste Shtand halde', +);
+
+/** Luxembourgish (Lëtzebuergesch)
+ * @author Robby
+ */
+$messages['lb'] = array(
+	'localisationupdate-desc' => 'hält déi lokaliséiert Messagen esou aktuell wéi \
méiglech.', +);
+
+/** Macedonian (Македонски)
+ * @author Bjankuloski06
+ */
+$messages['mk'] = array(
+	'localisationupdate-desc' => 'Ги одржува локализираните \
пораки колку што е можно пообновени и повеќе \
во тек со настаните', +);
+
+/** Malayalam (മലയാളം)
+ * @author Praveenp
+ */
+$messages['ml'] = array(
+	'localisationupdate-desc' => \
'പ്രാദേശികഭാഷയിലാക്കിയ \
സന്ദേശങ്ങൾ കഴിയുന്നത്ര വേഗം \
ചേർക്കാൻ ഉപയോഗിക്കുന്നു', +);
+
+/** Malay (Bahasa Melayu)
+ * @author Anakmalaysia
+ */
+$messages['ms'] = array(
+	'localisationupdate-desc' => 'Memastikan kekemaskinian mesej-mesej yang \
disetempatkan', +);
+
+/** Norwegian (bokmål)‬ (‪Norsk (bokmål)‬)
+ * @author Nghtwlkr
+ */
+$messages['nb'] = array(
+	'localisationupdate-desc' => 'Holder de lokaliserte meldingene så oppdaterte som \
mulig', +);
+
+/** Dutch (Nederlands)
+ * @author Siebrand
+ */
+$messages['nl'] = array(
+	'localisationupdate-desc' => 'Houdt de gelokaliseerde berichten zo actueel \
mogelijk', +);
+
+/** Norwegian Nynorsk (‪Norsk (nynorsk)‬)
+ * @author Gunnernett
+ */
+$messages['nn'] = array(
+	'localisationupdate-desc' => 'Held dei lokaliserte meldingane så oppdaterte som \
mogleg', +);
+
+/** Occitan (Occitan)
+ * @author Cedric31
+ */
+$messages['oc'] = array(
+	'localisationupdate-desc' => 'Manténer la traduccion dels messatges a jorn autant \
que possible', +);
+
+/** Polish (Polski)
+ * @author Sp5uhe
+ */
+$messages['pl'] = array(
+	'localisationupdate-desc' => 'Uaktualnia lokalne komunikaty w miarę możliwości \
na bieżąco', +);
+
+/** Piedmontese (Piemontèis)
+ * @author Dragonòt
+ */
+$messages['pms'] = array(
+	'localisationupdate-desc' => 'A manten i messagi localis  ël pì agiorn  \
possìbil', +);
+
+/** Portuguese (Português)
+ * @author Hamilton Abreu
+ * @author Malafaya
+ */
+$messages['pt'] = array(
+	'localisationupdate-desc' => 'Mantém as mensagens localizadas tão actualizadas \
quanto possível', +);
+
+/** Brazilian Portuguese (Português do Brasil)
+ * @author Eduardo.mps
+ */
+$messages['pt-br'] = array(
+	'localisationupdate-desc' => 'Mantém as mensagens localizadas tão atualizadas \
quanto possível', +);
+
+/** Romanian (Română)
+ * @author KlaudiuMihaila
+ */
+$messages['ro'] = array(
+	'localisationupdate-desc' => 'Menține mesajele localizate cât mai actualizate',
+);
+
+/** Tarandíne (Tarandíne)
+ * @author Joetaras
+ */
+$messages['roa-tara'] = array(
+	'localisationupdate-desc' => "Mandine le messagge localizzate 'u cchiù aggiornate \
possibbile", +);
+
+/** Russian ( усский)
+ * @author Александр Сигачёв
+ */
+$messages['ru'] = array(
+	'localisationupdate-desc' => 'Поддерживает актуальность \
локализованных сообщений, насколько это \
возможно', +);
+
+/** Slovak (Slovenčina)
+ * @author Helix84
+ */
+$messages['sk'] = array(
+	'localisationupdate-desc' => 'Udržiava lokalizované správy čo \
najaktuálnejšie', +);
+
+/** Serbian (Cyrillic script) (‪Српски (ћирилица)‬)
+ * @author Михајло Анђелковић
+ */
+$messages['sr-ec'] = array(
+	'localisationupdate-desc' => 'Ажурира локализоване поруке \
колико је то могуће', +);
+
+/** Serbian (Latin script) (‪Srpski (latinica)‬)
+ * @author Liangent
+ */
+$messages['sr-el'] = array(
+	'localisationupdate-desc' => 'Ažurira lokalizovane poruke koliko je to moguće',
+);
+
+/** Sundanese (Basa Sunda)
+ * @author Kandar
+ */
+$messages['su'] = array(
+	'localisationupdate-desc' => 'Ngajaga sangkan talatah-talatah nu geus \
dialihbasakeun salawasnya mutahir', +);
+
+/** Swedish (Svenska)
+ * @author Boivie
+ */
+$messages['sv'] = array(
+	'localisationupdate-desc' => 'Håller de lokaliserade meddelandena så uppdaterade \
som möjligt', +);
+
+/** Tamil (தமிழ்)
+ * @author செல்வா
+ */
+$messages['ta'] = array(
+	'localisationupdate-desc' => 'உட்சூழலுக்கான \
செய்திகளை கூடியமட்டிலும் \
இன்றையநிலையில் \
வைக்கப்பட்டுள்ளன', +);
+
+/** Telugu (తెలుగు)
+ * @author Veeven
+ */
+$messages['te'] = array(
+	'localisationupdate-desc' => 'స్ధానికీకరించిన \
సందేశాలను సాధ్యమైనంత తాజాగా \
ఉంచుతుంది', +);
+
+/** Tagalog (Tagalog)
+ * @author AnakngAraw
+ */
+$messages['tl'] = array(
+	'localisationupdate-desc' => 'Pinananatili ang mga mensaheng lokalisado bilang \
pinaka nasasapanahon', +);
+
+/** Turkish (Türkçe)
+ * @author Joseph
+ */
+$messages['tr'] = array(
+	'localisationupdate-desc' => 'Yerelleştirilen mesajları mümkün olabildiğince \
güncel tutar', +);
+
+/** Ukrainian (Українська)
+ * @author Prima klasy4na
+ */
+$messages['uk'] = array(
+	'localisationupdate-desc' => 'Забезпечує оновлення \
локалізованих повідомлень у міру можливості', \
+); +
+/** Veps (Vepsan kel')
+ * @author Игорь Бродский
+ */
+$messages['vep'] = array(
+	'localisationupdate-desc' => 'Pidab lokaliziruidud tedotused veresin, ku voib',
+);
+
+/** Vietnamese (Tiếng Việt)
+ * @author Vinhtantran
+ */
+$messages['vi'] = array(
+	'localisationupdate-desc' => 'Giữ các thông điệp bản địa hóa được \
cập nhật nhất có thể', +);
+
+/** Cantonese (粵語)
+ * @author Tom Maaswinkel
+ */
+$messages['yue'] = array(
+	'localisationupdate-desc' => '將本地化嘅信息保持最新',
+);
+
+/** Simplified Chinese (‪中文(简体)‬)
+ * @author Tom Maaswinkel
+ */
+$messages['zh-hans'] = array(
+	'localisationupdate-desc' => '将本地化的信息保持最新',
+);
+
+/** Traditional Chinese (‪中文(繁體)‬)
+ * @author Mark85296341
+ * @author Tom Maaswinkel
+ */
+$messages['zh-hant'] = array(
+	'localisationupdate-desc' => '將本地化的資訊盡可能保持最新',
+);
+
diff --git a/extensions/LocalisationUpdate/LocalisationUpdate.php \
b/extensions/LocalisationUpdate/LocalisationUpdate.php new file mode 100644
index 0000000..1dfdb88
--- /dev/null
+++ b/extensions/LocalisationUpdate/LocalisationUpdate.php
@@ -0,0 +1,51 @@
+<?php
+/*
+KNOWN ISSUES:
+- Only works with SVN revision 50605 or later of the
+  Mediawiki core
+- Requires file cache (see $wgLocalisationUpdateDirectory)
+*/
+
+// Configuration
+
+/**
+ * Directory to store serialized cache files in. Defaults to $wgCacheDirectory.
+ * It's OK to share this directory among wikis as long as the wiki you run
+ * update.php on has all extensions the other wikis using the same directory
+ * have.
+ * NOTE: If this variable and $wgCacheDirectory are both false, this extension
+ *       WILL NOT WORK.
+ */
+$wgLocalisationUpdateDirectory = false;
+
+
+/**
+ * This should point to either an HTTP-accessible Subversion repository containing
+ * MediaWiki's 'phase3' and 'extensions' directory, *or* a local directory \
containing + * checkouts of them:
+ *
+ * cd /path/to/mediawiki-trunk
+ * svn co http://svn.wikimedia.org/svnroot/mediawiki/trunk/phase3
+ * svn co http://svn.wikimedia.org/svnroot/mediawiki/trunk/extensions
+ * $wgLocalisationUpdateSVNURL = '/path/to/mediawiki-trunk';
+ */
+$wgLocalisationUpdateSVNURL = "http://svn.wikimedia.org/svnroot/mediawiki/trunk";
+
+$wgLocalisationUpdateRetryAttempts = 5;
+
+// Info about me!
+$wgExtensionCredits['other'][] = array(
+	'path'           => __FILE__,
+	'name'           => 'LocalisationUpdate',
+	'author'         => array( 'Tom Maaswinkel', 'Niklas Laxström', 'Roan Kattouw' ),
+	'version'        => '0.3',
+	'url'            => 'https://www.mediawiki.org/wiki/Extension:LocalisationUpdate',
+	'descriptionmsg' => 'localisationupdate-desc',
+);
+
+$wgHooks['LocalisationCacheRecache'][] = 'LocalisationUpdate::onRecache';
+
+$dir = dirname( __FILE__ ) . '/';
+$wgExtensionMessagesFiles['LocalisationUpdate'] = $dir . \
'LocalisationUpdate.i18n.php'; +$wgAutoloadClasses['LocalisationUpdate'] = $dir . \
'LocalisationUpdate.class.php'; +$wgAutoloadClasses['QuickArrayReader'] = $dir . \
                'QuickArrayReader.php';
diff --git a/extensions/LocalisationUpdate/QuickArrayReader.php \
b/extensions/LocalisationUpdate/QuickArrayReader.php new file mode 100644
index 0000000..214d5a6
--- /dev/null
+++ b/extensions/LocalisationUpdate/QuickArrayReader.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * Quickie parser class that can happily read the subset of PHP we need
+ * for our localization arrays safely.
+ *
+ * About an order of magnitude faster than ConfEditor(), but still an
+ * order of magnitude slower than eval().
+ */
+class QuickArrayReader {
+	var $vars = array();
+
+	/**
+	 * @param $string string
+	 */
+	function __construct( $string ) {
+		$scalarTypes = array(
+			T_LNUMBER => true,
+			T_DNUMBER => true,
+			T_STRING => true,
+			T_CONSTANT_ENCAPSED_STRING => true,
+		);
+		$skipTypes = array(
+			T_WHITESPACE => true,
+			T_COMMENT => true,
+			T_DOC_COMMENT => true,
+		);
+		$tokens = token_get_all( $string );
+		$count = count( $tokens );
+		for( $i = 0; $i < $count; ) {
+			while( isset($skipTypes[$tokens[$i][0]] ) ) {
+				$i++;
+			}
+			switch( $tokens[$i][0] ) {
+			case T_OPEN_TAG:
+				$i++;
+				continue;
+			case T_VARIABLE:
+				// '$messages' -> 'messages'
+				$varname = trim( substr( $tokens[$i][1], 1 ) );
+				$varindex = null;
+
+				while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+				if( $tokens[$i] === '[' ) {
+					while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+					if( isset($scalarTypes[$tokens[$i][0]] ) ) {
+						$varindex = $this->parseScalar( $tokens[$i] );
+					} else {
+						throw $this->except( $tokens[$i], 'scalar index' );
+					}
+					while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+					if( $tokens[$i] !== ']' ) {
+						throw $this->except( $tokens[$i], ']' );
+					}
+					while( isset($skipTypes[$tokens[++$i][0]] ) );
+				}
+
+				if( $tokens[$i] !== '=' ) {
+					throw $this->except( $tokens[$i], '=' );
+				}
+				while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+				if( isset($scalarTypes[$tokens[$i][0]] ) ) {
+					$buildval = $this->parseScalar( $tokens[$i] );
+				} elseif( $tokens[$i][0] === T_ARRAY ) {
+					while( isset($skipTypes[$tokens[++$i][0]] ) );
+					if( $tokens[$i] !== '(' ) {
+						throw $this->except( $tokens[$i], '(' );
+					}
+					$buildval = array();
+					do {
+						while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+						if( $tokens[$i] === ')' ) {
+							break;
+						}
+						if( isset($scalarTypes[$tokens[$i][0]] ) ) {
+							$key = $this->parseScalar( $tokens[$i] );
+						}
+						while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+						if( $tokens[$i][0] !== T_DOUBLE_ARROW ) {
+							throw $this->except( $tokens[$i], '=>' );
+						}
+						while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+						if( isset($scalarTypes[$tokens[$i][0]] ) ) {
+							$val = $this->parseScalar( $tokens[$i] );
+						}
+						@$buildval[$key] = $val;
+						while( isset($skipTypes[$tokens[++$i][0]] ) );
+
+						if( $tokens[$i] === ',' ) {
+							continue;
+						} elseif( $tokens[$i] === ')' ) {
+							break;
+						} else {
+							throw $this->except( $tokens[$i], ', or )' );
+						}
+					} while(true);
+				} else {
+					throw $this->except( $tokens[$i], 'scalar or array' );
+				}
+				if( is_null( $varindex ) ) {
+					$this->vars[$varname] = $buildval;
+				} else {
+					@$this->vars[$varname][$varindex] = $buildval;
+				}
+				while( isset($skipTypes[$tokens[++$i][0]] ) );
+				if( $tokens[$i] !== ';' ) {
+					throw $this->except($tokens[$i], ';');
+				}
+				$i++;
+				break;
+			default:
+				throw $this->except($tokens[$i], 'open tag, whitespace, or variable.');
+			}
+		}
+	}
+
+	/**
+	 * @param $got string
+	 * @param $expected string
+	 * @return Exception
+	 */
+	private function except( $got, $expected ) {
+		if( is_array( $got ) ) {
+			$got = token_name( $got[0] ) . " ('" . $got[1] . "')";
+		} else {
+			$got = "'" . $got . "'";
+		}
+		return new Exception( "Expected $expected, got $got" );
+	}
+
+	/**
+	 * Parse a scalar value in PHP
+	 *
+	 * @param $token string
+	 *
+	 * @return mixed Parsed value
+	 */
+	function parseScalar( $token ) {
+		if( is_array( $token ) ) {
+			$str = $token[1];
+		} else {
+			$str = $token;
+		}
+		if ( $str !== '' && $str[0] == '\'' )
+			// Single-quoted string
+			// @fixme trim() call is due to mystery bug where whitespace gets
+			// appended to the token; without it we ended up reading in the
+			// extra quote on the end!
+			return strtr( substr( trim( $str ), 1, -1 ),
+				array( '\\\'' => '\'', '\\\\' => '\\' ) );
+		if ( $str !== '' && @$str[0] == '"' )
+			// Double-quoted string
+			// @fixme trim() call is due to mystery bug where whitespace gets
+			// appended to the token; without it we ended up reading in the
+			// extra quote on the end!
+			return stripcslashes( substr( trim( $str ), 1, -1 ) );
+		if ( substr( $str, 0, 4 ) === 'true' )
+			return true;
+		if ( substr( $str, 0, 5 ) === 'false' )
+			return false;
+		if ( substr( $str, 0, 4 ) === 'null' )
+			return null;
+		// Must be some kind of numeric value, so let PHP's weak typing
+		// be useful for a change
+		return $str;
+	}
+
+	/**
+	 * @param $varname string
+	 * @return null|string
+	 */
+	function getVar( $varname ) {
+		if( isset( $this->vars[$varname] ) ) {
+			return $this->vars[$varname];
+		} else {
+			return null;
+		}
+	}
+}
+
diff --git a/extensions/LocalisationUpdate/README_FIRST.txt \
b/extensions/LocalisationUpdate/README_FIRST.txt new file mode 100644
index 0000000..3973c43
--- /dev/null
+++ b/extensions/LocalisationUpdate/README_FIRST.txt
@@ -0,0 +1,8 @@
+To install this extension first include
+LocalisationUpdate/LocalisationUpdate.php in your LocalSettings.php
+
+Then add the required new tables to your database by running
+php maintenance/update.php on the command line.
+
+Whenever you want to run an update, run
+php extensions/LocalisationUpdate/update.php on the command line.
diff --git a/extensions/LocalisationUpdate/tests/tokenTest.php \
b/extensions/LocalisationUpdate/tests/tokenTest.php new file mode 100644
index 0000000..1112313
--- /dev/null
+++ b/extensions/LocalisationUpdate/tests/tokenTest.php
@@ -0,0 +1,91 @@
+<?php
+
+$IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
+	? getenv( 'MW_INSTALL_PATH' )
+	: realpath( dirname( __FILE__ ) . "/../../../" );
+
+require_once( "$IP/maintenance/commandLine.inc" );
+
+function evalExtractArray( $php, $varname ) {
+	eval( $php );
+	return @$$varname;
+}
+
+function confExtractArray( $php, $varname ) {
+	try {
+		$ce = new ConfEditor("<?php $php");
+		$vars = $ce->getVars();
+		$retval = @$vars[$varname];
+	} catch( Exception $e ) {
+		print $e . "\n";
+		$retval = null;
+	}
+	return $retval;
+}
+
+function quickTokenExtractArray( $php, $varname ) {
+	$reader = new QuickArrayReader("<?php $php");
+	return $reader->getVar( $varname );
+}
+
+
+if( count( $args ) ) {
+	$sources = $args;
+} else {
+	$sources =
+		array_merge(
+			glob("$IP/extensions/*/*.i18n.php"),
+			glob("$IP/languages/messages/Messages*.php") );
+}
+
+foreach( $sources as $sourceFile ) {
+	$rel = basename( $sourceFile );
+	$out = str_replace( '/', '-', $rel );
+
+	$sourceData = file_get_contents( $sourceFile );
+
+	if( preg_match( '!extensions/!', $sourceFile ) ) {
+		$sourceData = LocalisationUpdate::cleanupExtensionFile( $sourceData );
+		$items = 'langs';
+	} else {
+		$sourceData = LocalisationUpdate::cleanupFile( $sourceData );
+		$items = 'messages';
+	}
+
+	file_put_contents( "$out.txt", $sourceData );
+
+	$start = microtime(true);
+	$eval = evalExtractArray( $sourceData, 'messages' );
+	$deltaEval = microtime(true) - $start;
+
+	$start = microtime(true);
+	$quick = quickTokenExtractArray( $sourceData, 'messages' );
+	$deltaQuick = microtime(true) - $start;
+
+	$start = microtime(true);
+	$token = confExtractArray( $sourceData, 'messages' );
+	$deltaToken = microtime(true) - $start;
+
+	$hashEval = md5(serialize($eval));
+	$hashToken = md5(serialize($token));
+	$hashQuick = md5(serialize($quick));
+	$countEval = count( (array)$eval);
+	$countToken = count( (array)$token );
+	$countQuick = count( (array)$quick );
+
+	printf( "%s %s %d $items - %0.1fms - eval\n", $rel, $hashEval, $countEval, \
$deltaEval * 1000 ); +	printf( "%s %s %d $items - %0.1fms - QuickArrayReader\n", \
$rel, $hashQuick, $countQuick, $deltaQuick * 1000 ); +	printf( "%s %s %d $items - \
%0.1fms - ConfEditor\n", $rel, $hashToken, $countToken, $deltaToken * 1000 ); +
+	if( $hashEval !== $hashToken || $hashEval !== $hashQuick ) {
+		echo "FAILED on $rel\n";
+		file_put_contents( "$out-eval.txt", var_export( $eval, true ) );
+		file_put_contents( "$out-token.txt", var_export( $token, true ) );
+		file_put_contents( "$out-quick.txt", var_export( $quick, true ) );
+		#die("check eval.txt and token.txt\n");
+	}
+	echo "\n";
+}
+
+echo "ok\n";
+
diff --git a/extensions/LocalisationUpdate/update.php \
b/extensions/LocalisationUpdate/update.php new file mode 100644
index 0000000..649fc86
--- /dev/null
+++ b/extensions/LocalisationUpdate/update.php
@@ -0,0 +1,38 @@
+<?php
+
+$IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
+	? getenv( 'MW_INSTALL_PATH' )
+	: realpath( dirname( __FILE__ ) . "/../../" );
+
+// TODO: migrate to maintenance class
+require_once( "$IP/maintenance/commandLine.inc" );
+
+if( isset( $options['help'] ) ) {
+	print "Fetches updated localisation files from MediaWiki development SVN\n";
+	print "and saves into local database to merge with release defaults.\n";
+	print "\n";
+	print "Usage: php extensions/LocalisationUpdate/update.php\n";
+	print "Options:\n";
+	print "  --quiet           Suppress progress output\n";
+	print "  --skip-core       Don't fetch MediaWiki core files\n";
+	print "  --skip-extensions Don't fetch any extension files\n";
+	print "  --all             Fetch all present extensions, not just those enabled\n";
+	print "  --outdir=<dir>    Override output directory for serialized update \
files\n"; +	print "  --svnurl=<url>    URL to SVN repository, or path to local SVN \
checkout. Default: $wgLocalisationUpdateSVNURL\n"; +	print "\n";
+	exit( 0 );
+}
+
+
+$starttime = microtime( true );
+
+// Prevent the script from timing out
+set_time_limit( 0 );
+ini_set( "max_execution_time", 0 );
+ini_set( 'memory_limit', -1 );
+
+LocalisationUpdate::updateMessages( $options );
+
+$endtime = microtime( true );
+$totaltime = ( $endtime - $starttime );
+print "All done in " . $totaltime . " seconds\n";


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic