Index: extend/plugins-plugins/svn-track/class.dotorg-plugins-i18n.php
===================================================================
--- extend/plugins-plugins/svn-track/class.dotorg-plugins-i18n.php	(revision 0)
+++ extend/plugins-plugins/svn-track/class.dotorg-plugins-i18n.php	(working copy)
@@ -0,0 +1,658 @@
+<?php
+class Dotorg_Plugin_I18n {
+	var $db;              // Set in __construct()
+	var $tracker;         // Set in __construct()
+	var $master_project   = 'wp-plugins';
+	var $i18n_cache_group = 'plugins-i18n';
+
+	// TODO: remove when we launch for all plugins.
+	var $translated_plugins = array(
+		'akismet', 'wpcat2tag-importer', 'wordpress-importer',
+		'utw-importer', 'textpattern-importer', 'stp-importer',
+		'rss-importer', 'opml-importer', 'movabletype-importer',
+		'livejournal-importer', 'greymatter-importer', 'dotclear-importer',
+		'blogware-importer', 'blogger-importer', 'tumblr-importer',
+		'bbpress', 'wordpress-beta-tester', 'theme-check'
+	);
+
+	function __construct( $db, $tracker = null ) {
+		if ( !empty( $db ) && is_object( $db ) )
+			$this->db = $db;
+		if ( !empty( $tracker ) && is_object( $tracker ) )
+			$this->tracker = $tracker;
+		wp_cache_add_global_groups( $this->i18n_cache_group );
+	}
+
+	/*
+	 * ***********************
+	 * Processing
+	 * ***********************
+	 */
+
+	function process( $slug, $branch = 'dev', $type = 'all' ) {
+		$slug = trim( $slug, '/' );
+
+		if ( empty( $slug ) || false !== strpos( $slug, '/' ) || empty( $this->tracker ) )
+			return false;
+
+		// DEBUG: in_array check is because we'll start the program with a finite list of plugins
+		// TODO: remove when we launch for all plugins.
+		if ( ! in_array( $slug, $this->translated_plugins ) )
+			return false;
+
+		if ( 'stable' !== $branch )
+			$branch = 'dev';
+
+		if ( 'code' !== $type && 'readme' !== $type )
+			$type = 'all';
+
+		$path_rel = "{$slug}/trunk/";
+
+		if ( 'stable' === $branch ) {
+			if ( false == ( $stable_tag = $this->tracker->get_stable_tag_dir_using( $path_rel ) ) ) {
+				// Can't get a stable tag, bail out
+				return false;
+			} else if ( 'trunk' == trim( $stable_tag['tag_dir'], '/' ) ) {
+				// If stable is trunk, then it's really same as dev, switch to that
+				$branch = 'dev';
+			} else {
+				// We're dealing with an actual stable tag, go for it
+				$path_rel = "{$slug}/{$stable_tag['tag_dir']}"; //
+			}
+		}
+
+		// Ensure that GlotPress is all set for the plugin
+		$this->set_glotpress_for_plugin( $slug );
+
+		if ( 'code' === $type || 'all' === $type )
+			$this->process_code( $path_rel, $branch );
+
+		if ( 'readme' === $type || 'all' === $type )
+			$this->process_readme( $path_rel, $branch );
+
+		echo "Processed {$type} for {$path_rel}\n";
+		return true;
+	}
+
+	function process_code( $path_rel, $branch = 'dev' ) {
+		if ( empty( $this->tracker ) )
+			return false;
+
+		$slug = preg_replace( '|^/?([^/]+)/?.+?$|', '\1', $path_rel );
+
+		if ( empty( $slug ) || !preg_match( '/^[a-z0-9-]+$/i', $slug ) )
+			return false;
+
+		// DEBUG: in_array check is because we'll start the program with a finite list of plugins
+		// TODO: remove when we launch for all plugins.
+		if ( !in_array( $slug, $this->translated_plugins ) )
+			return false;
+
+		$export_path = $this->tracker->create_export( $path_rel );
+
+		if ( empty( $export_path ) || !is_dir( $export_path ) )
+			return false;
+
+		$old_cwd = getcwd();
+		chdir( $export_path );
+
+		// Check for a plugin text domain declaration and loading, grep recursively, not necessarily in [slug].php
+		if ( ! shell_exec( 'grep -r --include "*.php" "Text Domain: ' . escapeshellarg( $slug ) . '" .' ) && ! shell_exec( 'grep -r --include "*.php" "\bload_plugin_textdomain\b" .' ) )
+			return false;
+
+		if ( !class_exists( 'PotExtMeta' ) )
+			require_once( __DIR__ . '/i18n-tools/pot-ext-meta.php' );
+
+		// Create pot file from code
+		$pot_file = "./tmp-{$slug}.pot"; // Using tmp- prefix in case a plugin has $slug.pot committed
+		$makepot  = new MakePOT;
+
+		if ( ! $makepot->wp_plugin( '.', $pot_file, $slug ) || ! file_exists( $pot_file ) )
+			return false;
+
+		// DEBUG
+		// system( "cat {$pot_file}" );
+
+		$this->import_to_glotpress_project( $slug, $branch, $pot_file );
+
+		chdir( $old_cwd );
+		return true;
+	}
+
+	function process_readme( $path_rel, $branch = 'dev' ) {
+		if ( empty( $this->tracker ) )
+			return false;
+
+		$slug = preg_replace( '|^/?([^/]+)/?.+?$|', '\1', $path_rel );
+
+		if ( empty( $slug ) || !preg_match( '/^[a-z0-9-]+$/i', $slug ) )
+			return false;
+
+		// DEBUG: in_array as separate check because we'll start the program with a finite list of plugins
+		// TODO: remove when we launch for all plugins.
+		if ( !in_array( $slug, $this->translated_plugins ) )
+			return false;
+
+		$export_path = $this->tracker->create_export( $path_rel );
+
+		if ( empty( $export_path ) || !is_dir( $export_path ) )
+			return false;
+
+		$old_cwd = getcwd();
+		chdir( $export_path );
+
+		$readme = $this->tracker->parse_readme_in( $path_rel );
+
+		$str_priorities = array();
+
+		if ( !class_exists( 'PO' ) )
+			require_once( __DIR__ . '/i18n-tools/pomo/po.php' );
+
+		$pot = new PO;
+
+		// No need for license, being in the directory implies GPLv2 or later. Add here otherwise.
+		foreach ( array( 'name', 'short_description' ) as $key ) {
+			$readme[ $key ] = trim( $readme[ $key ] ) ;
+		}
+
+		// If empty or "sketchy", get the plugin name form the PHP file's headers
+		if ( empty( $readme['name'] ) || 'Plugin Name' == trim( $readme['name'] ) ) {
+			// -o in grep will make sure we don't get comments opening delimiters (//, /*) or spaces as part of string
+			$name_from_php  = trim( shell_exec( 'grep -o "\bPlugin Name:.*" ' . escapeshellarg( $slug ) . '.php' ) );
+			// Remove the header label
+			$name_from_php  = str_replace( 'Plugin Name:', '', $name_from_php );
+			// Do clean out potential comment closing delimiter (*/) out of string
+			$name_from_php  = preg_replace( '|^(.+)[\s]+?\*/$|', '\1', $name_from_php );
+			// Use trimmed results as plugin name
+			$readme['name'] = trim( $name_from_php );
+		}
+
+		if ( !empty( $readme['name'] ) ) {
+			$pot->add_entry( new Translation_Entry ( array(
+				'singular'           => $readme['name'],
+				'extracted_comments' => 'Name.',
+			) ) );
+
+			$str_priorities[ $readme['name'] ] = 1;
+		}
+
+		if ( !empty( $readme['short_description'] ) ) {
+			$pot->add_entry( new Translation_Entry ( array(
+				'singular'           => $readme['short_description'],
+				'extracted_comments' => 'Short description.',
+			) ) );
+
+			$str_priorities[ $readme['short_description'] ] = 1;
+		}
+
+		if ( !empty( $readme['screenshots'] ) ) {
+			foreach ( $readme['screenshots'] as $sshot_key => $sshot_desc ) {
+				$sshot_desc = trim( $sshot_desc );
+				$pot->add_entry( new Translation_Entry ( array(
+					'singular'           => $sshot_desc,
+					'extracted_comments' => 'Screenshot description.',
+				) ) );
+			}
+
+		}
+
+		if ( empty( $readme['sections'] ) )
+			$readme['sections'] = array();
+
+		// Adding remaining content as a section so it's processed by the same loop below
+		if ( !empty( $readme['remaining_content'] ) )
+			$readme['sections']['remaining_content'] = $readme['remaining_content'];
+
+		$strings = array();
+
+		foreach ( $readme['sections'] as $section_key => $section_text ) {
+			if ( 'screenshots' == $section_key )
+				continue;
+
+			/*
+			 * Scanned tags based on block elements found in Automattic_Readme::filter_text() $allowed.
+			 * Scanning H3/4, li, p and blockquote. Other tags are ignored  in strings (a, strong, cite, etc).
+			 * Processing notes:
+			 * * Don't normalize/modify original text, will be used as a search pattern in original doc in some use-cases.
+			 * * Using regexes over XML parsing for performance reasons, could move to the latter for more accuracy.
+			 */
+
+			if ( 'changelog' !== $section_key ) { // No need to scan non-translatable version headers in changelog
+				if ( preg_match_all( '|<h[3-4]+[^>]*>(.+)</h[3-4]+>|', $section_text, $matches ) ) {
+					if ( !empty( $matches[1] ) ) {
+						foreach ( $matches[1] as $text ) {
+							$strings = $this->handle_translator_comment( $strings, $text, "{$section_key} header" );
+						}
+					}
+				}
+			}
+
+			if ( preg_match_all( '|<li>(.+)</li>|', $section_text, $matches ) ) {
+				if ( !empty( $matches[1] ) ) {
+					foreach ( $matches[1] as $text ) {
+						$strings = $this->handle_translator_comment( $strings, $text, "{$section_key} list item" );
+						if ( 'changelog' === $section_key )
+							$str_priorities[ $text ] = -1;
+					}
+				}
+			}
+
+			if ( preg_match_all( '|<p>(.+)</p>|', $section_text, $matches ) ) {
+				if ( !empty( $matches[1] ) ) {
+					foreach ( $matches[1] as $text ) {
+						$strings = $this->handle_translator_comment( $strings, $text, "{$section_key} paragraph" );
+						if ( 'changelog' === $section_key )
+							$str_priorities[ $text ] = -1;
+					}
+				}
+			}
+
+			if ( preg_match_all( '|<blockquote>(.+)</blockquote>|', $section_text, $matches ) ) {
+				if ( !empty( $matches[1] ) ) {
+					foreach ( $matches[1] as $text ) {
+						$strings = $this->handle_translator_comment( $strings, $text, "{$section_key} block quote" );
+						if ( 'changelog' === $section_key )
+							$str_priorities[ $text ] = -1;
+					}
+				}
+			}
+		}
+
+		foreach ( $strings as $text => $comments ) {
+			$pot->add_entry( new Translation_Entry ( array(
+				'singular'           => $text,
+				'extracted_comments' => 'Found in ' . implode( $comments, ", " ) . '.',
+			) ) );
+		}
+
+		$pot_file = "./tmp-{$slug}-readme.pot";
+		$pot->export_to_file( $pot_file );
+
+		// DEBUG
+		// system( "cat {$pot_file}" );
+
+		// import to GlotPress, dev or stable
+		$this->import_to_glotpress_project( $slug, "{$branch}-readme", $pot_file, $str_priorities );
+
+		chdir( $old_cwd );
+		return true;
+	}
+
+	function handle_translator_comment( $array, $key, $val ) {
+		$val = trim( preg_replace( '/[^a-z0-9]/i', ' ', $val ) ); // cleanup key names for display
+		if ( empty( $array[ $key ] ) ) {
+			$array[ $key ] = array( $val );
+		} else if ( !in_array( $val, $array[ $key ] ) ) {
+			$array[ $key ][] = $val;
+		}
+		return $array;
+	}
+
+	function import_to_glotpress_project( $project, $branch, $file, $str_priorities = array() ) {
+		if ( empty( $project ) || empty( $branch ) || empty( $file ) )
+			return;
+		// Note: this will only work if the GlotPress project/sub-projects exist.
+		$cmd = 'php ' . __DIR__ . '/../../../translate/glotpress/scripts/import-originals.php -o po -p ' . escapeshellarg( "wp-plugins/{$project }/{$branch}" ) . ' -f ' . escapeshellarg( $file );
+		// DEBUG
+		// var_dump( $cmd );
+		system( $cmd );
+		if ( empty( $str_priorities ) )
+			return;
+		$branch_id = $this->get_gp_branch_id( $project, $branch );
+		// Set the string priorities in GP once the originals have been imported
+		if ( empty( $branch_id ) )
+			return;
+		foreach ( (array) $str_priorities as $str => $prio ) {
+			if ( 1 !== $prio && -1 !== $prio )
+				$prio = 0;
+			$res = $this->db->query( $this->db->prepare(
+				'UPDATE translate_originals SET priority = %d WHERE project_id = %d AND status = %s AND singular = %s',
+				$prio, $branch_id, '+active', $str
+			) );
+		}
+	}
+
+	function set_glotpress_for_plugin( $plugin_slug ) {
+		if ( empty( $plugin_slug ) )
+			return;
+		$cmd = 'php ' . __DIR__ . '/../../../translate/bin/set-wp-plugin-project.php ' . escapeshellarg( $plugin_slug );
+		// DEBUG
+		// var_dump( $cmd );
+		system( $cmd );
+	}
+
+	function process_paths_array( $paths ) {
+		if ( empty( $paths ) || !is_array( $paths ) )
+			return;
+
+		$to_process = array();
+
+		// Note: never assume all passed paths are for the same plugin or branch (dev vs stable)
+		foreach( $paths as $path ) {
+			// Start with the quickest tests
+			if ( !preg_match( '/^[^\/]+\/(trunk|tags)\/.*$/', $path ) )
+				continue; // affected path doesn't call for reprocessing
+
+			if ( preg_match( '/\/readme\.txt$/', $path ) ) {
+				$type = 'readme';
+			} else if ( preg_match( '/\.php$/', $path ) ) {
+				$type = 'code';
+			} else if ( preg_match( '/\/$/', $path ) || preg_match( '/\/[^\.]+$/', $path ) ) {
+				// Dealing with a directory (or extension-less file)
+				if ( preg_match( '/^[^\/]+\/(trunk|tags\/[^\/]+)\/?$/', $path ) ) {
+					// Top level trunk or tagged release directory, process both code and readme
+					$type = 'all';
+				} else {
+					// Some other subdirectory that could contain code, only process the latter
+					$type = 'code';
+				}
+			} else {
+				continue; // affected path doesn't call for reprocessing
+			}
+
+			$plugin = preg_replace( '/^([^\/]+)(\/.*)?$/', '\1', $path );
+
+			if ( empty( $plugin ) )
+				continue;
+
+			// Finish with branch definition/test, since potentially the slowest, through $this->tracker->get_stable_tag_dir_using()
+			if ( false !== strpos( $path, "{$plugin}/trunk/" ) ) {
+				$branch = 'dev';
+			} else {
+				// Only process non-trunk revs if they are part of the latest stable tag
+				$latest_stable_tag = $this->tracker->get_stable_tag_dir_using( $plugin );
+				$expected_tag_path = "{$plugin}/tags/{$latest_stable_tag}";
+				if ( substr( $path, 0, strlen( $expected_tag_path ) ) === $expected_tag_path ) {
+					$branch = 'stable';
+				}
+			}
+
+			if ( empty( $branch ) )
+				continue; // affected path doesn't call for reprocessing
+
+			// Using array keys so we only end up processing every combo once in a log
+			$to_process[ $plugin ][ $branch ][ $type ] = true;
+		}
+
+		if ( empty( $to_process ) )
+			return;
+
+		// Now, process the plugin + branch + type combos, only once each
+		foreach( $to_process as $plugin => $branches ) {
+			foreach( $branches as $branch => $types ) {
+				// Consolidate types into "all" if requesting both code and readme
+				if ( isset( $types[ 'code' ] ) && isset( $types[ 'readme' ] ) ) {
+					$types[ 'all' ] = true;
+				}
+				// Late cleanup, to once again insure minimal reprocessing
+				if ( isset( $types[ 'all' ] ) ) {
+					unset( $types[ 'code' ] );
+					unset( $types[ 'readme' ] );
+				}
+				// Go for processing
+				foreach( $types as $type => $unused ) {
+					$this->process( $plugin, $branch, $type );
+				}
+			}
+		}
+	}
+
+	/*
+	 * ***********************
+	 * Rendering
+	 * ***********************
+	 */
+
+	function translate( $key, $content, $args = array() ) {
+		if ( empty( $key ) || empty( $content ) )
+			return $content;
+
+		if ( !empty( $args['topic_id'] ) && is_numeric( $args['topic_id'] ) )
+			$topic = get_topic( $args['topic_id'] );
+		else
+			global $topic;
+
+		if ( empty( $topic ) )
+			return $content;
+
+		$language     = '';
+		$server_name  = strtolower( $_SERVER['SERVER_NAME'] );
+		if ( 'api.wordpress.org' == $server_name ) {
+			if ( preg_match( '/^[a-z]{2}(_[A-Z]{2})?$/', trim( $_REQUEST['locale'] ) ) ) {
+				$language = substr( trim( $_REQUEST['locale'] ), 0, 5 );
+			} else if ( !empty( $_REQUEST['request'] ) ) {
+				$request = maybe_unserialize( $_REQUEST['request'] );
+				if ( !empty( $request ) && !empty( $request->locale ) && preg_match( '/^[a-z]{2}(_[A-Z]{2})?$/', trim( $request->locale ) )  ) {
+					$language = trim( $request->locale );
+				}
+			}
+		} else if ( preg_match( '/^([^\.]+)\.wordpress\.org$/', $server_name, $matches ) ) {
+			$subdomain = $this->verify_subdomain( $matches[1] );
+			if ( ! empty( $subdomain ) )
+				$language = substr( $server_name, 0, 2 );
+		}
+
+		if ( empty( $language ) || 'en' === $language || 'en_US' === $language )
+			return $content;
+
+		$slug = $topic->plugin_san;
+
+		// DEBUG: in_array check is because we'll start the program with a finite list of plugins
+		// TODO: remove when we launch for all plugins.
+		if ( empty( $slug ) || ! in_array( $slug, $this->translated_plugins ) )
+			return $content;
+
+		$branch = ( empty( $topic->stable_tag ) || 'trunk' === $topic->stable_tag ) ? 'dev' : 'stable';
+
+		if ( empty( $args['code_i18n'] ) || true !== $args['code_i18n'] )
+			$branch .= '-readme';
+
+		$cache_suffix = "{$language}:{$key}";
+
+		// Try the cache
+		if ( false !== ( $cache = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) {
+			// DEBUG
+			// var_dump( array( $slug, $branch, $cache_suffix, $cache ) );
+			return $cache;
+		}
+
+		$originals = $this->get_gp_originals( $slug, $branch, $key, $content );
+
+		if ( empty( $originals ) )
+			return $content;
+
+		$translation_set_id = $this->get_gp_translation_set_id( $slug, $branch, $language );
+
+		if ( empty( $translation_set_id ) )
+			return $content;
+
+		foreach ( $originals as $original ) {
+			if ( empty( $original->id ) )
+				continue;
+
+			$translation = $this->db->get_var( $this->db->prepare(
+				'SELECT translation_0 FROM translate_translations WHERE original_id = %d AND translation_set_id = %d AND status = %s',
+				$original->id, $translation_set_id, 'current'
+			) );
+
+			if ( empty( $translation ) )
+				continue;
+
+			$content = $this->translate_gp_original( $original->singular, $translation, $content );
+		}
+
+		$this->cache_set( $slug, $branch, $content, $cache_suffix );
+
+		return $content;
+	}
+
+	function cache_key( $slug, $branch, $suffix = null ) {
+		// EG keys
+		// plugin:press-this:stable-readme:originals
+		// plugin:press-this:stable-readme:original:title
+		// plugin:press-this:stable-readme:fr:title
+		$key = "{$this->master_project}:{$slug}:{$branch}";
+		if ( !empty( $suffix ) )
+			$key .= ":{$suffix}";
+		return $key;
+	}
+
+	function cache_get( $slug, $branch, $suffix = null ) {
+		$key = $this->cache_key( $slug, $branch, $suffix );
+		// DEBUG
+		// wp_cache_delete( $key, $this->i18n_cache_group );
+		return wp_cache_get( $key, $this->i18n_cache_group );
+	}
+
+	function cache_set( $slug, $branch, $content, $suffix = null ) {
+		$key = $this->cache_key( $slug, $branch, $suffix );
+		return wp_cache_set( $key, $content, $this->i18n_cache_group );
+	}
+
+	function get_gp_branch_id( $slug, $branch ) {
+		$cache_suffix = "branch_id";
+
+		if ( false !== ( $branch_id = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
+			return $branch_id;
+
+		$branch_id = $this->db->get_var( $this->db->prepare(
+			'SELECT id FROM translate_projects WHERE path = %s',
+			"wp-plugins/{$slug}/{$branch}"
+		) );
+
+		if ( empty( $branch_id ) )
+			$branch_id = 0;
+
+		$this->cache_set( $slug, $branch, $branch_id, $cache_suffix );
+
+		return $branch_id;
+	}
+
+	function get_gp_originals( $slug, $branch, $key, $str ) {
+		// Try to get a single original with the whole content first (title, etc), if passed, or get them all otherwise.
+		if ( !empty( $key ) && !empty( $str ) ) {
+			$originals = $this->search_gp_original( $slug, $branch, $key, $str );
+			if ( !empty( $originals ) )
+				return array( $originals );
+			// Do not cache this as originals, search_gp_original() does its own caching
+		}
+
+		$cache_suffix = 'originals';
+
+		if ( false !== ( $originals = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
+			return $originals;
+
+		$branch_id = $this->get_gp_branch_id( $slug, $branch );
+
+		if ( empty( $branch_id ) )
+			return array();
+
+		$originals = $this->db->get_results( $this->db->prepare(
+			'SELECT id, singular, comment FROM translate_originals WHERE project_id = %d AND status = %s',
+			$branch_id, '+active'
+		) );
+
+		if ( empty( $originals ) )
+			$originals = array(); // still cache if empty, but as array, never false
+
+		$this->cache_set( $slug, $branch, $originals, $cache_suffix );
+
+		return $originals;
+	}
+
+	function get_gp_translation_set_id( $slug, $branch, $locale ) {
+		$locale = strtolower( $locale );
+
+		if ( false !== strpos( $locale, '_' ) ) {
+			$locale = str_replace( '_', '-', $locale );
+		}
+
+		$cache_suffix = "{$locale}:translation_set_id";
+
+		if ( false !== ( $translation_set_id = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
+			return $translation_set_id;
+
+		$branch_id = $this->get_gp_branch_id( $slug, $branch );
+
+		if ( empty( $branch_id ) )
+			return 0;
+
+		$translation_set_id = $this->db->get_var( $this->db->prepare(
+			'SELECT id FROM translate_translation_sets WHERE project_id = %d AND locale = %s',
+			$branch_id, $locale ) );
+
+		if ( empty( $translation_set_id ) ) {
+			// Don't give up yet. Might be given fr_FR, which actually exists as locale=fr in GP.
+			$translation_set_id = $this->db->get_var( $this->db->prepare(
+				'SELECT id FROM translate_translation_sets WHERE project_id = %d AND locale = %s',
+				$branch_id, substr( $locale, 0, 2 ) ) );
+		}
+
+		if ( empty( $translation_set_id ) )
+			$translation_set_id = 0;
+
+		$this->cache_set( $slug, $branch, $translation_set_id, $cache_suffix );
+
+		return $translation_set_id;
+	}
+
+	function search_gp_original( $slug, $branch, $key, $str ) {
+		$cache_suffix = "original:{$key}";
+
+		if ( false !== ( $original = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
+			return $original;
+
+		$branch_id = $this->get_gp_branch_id( $slug, $branch );
+
+		if ( empty( $branch_id ) )
+			return false;
+
+		$original = $this->db->get_row( $this->db->prepare(
+			'SELECT id, singular, comment FROM translate_originals WHERE project_id = %d AND status = %s AND singular = %s',
+			$branch_id, '+active', $str
+		) );
+
+		if ( empty( $original ) )
+			$original = null;
+
+		$this->cache_set( $slug, $branch, $original, $cache_suffix );
+
+		return $original;
+	}
+
+	function translate_gp_original( $original, $translation, $content) {
+		$content = str_replace( $original, $translation, $content );
+		return $content;
+	}
+
+	function verify_subdomain( $locale ) {
+		if ( empty( $locale ) )
+			return '';
+
+		$cache_key = "subdomains:{$locale}";
+
+		wp_cache_delete( $cache_key, $this->i18n_cache_group );
+		if ( false !== ( $subdomain = wp_cache_get( $cache_key, $this->i18n_cache_group ) ) ) {
+			// var_dump(array($cache_key, $subdomain));
+			return $subdomain;
+		}
+
+		$subdomain = '';
+
+		if ( 2 === strlen( $locale ) ) {
+			$subdomain = $this->db->get_var( $this->db->prepare(
+				'SELECT subdomain FROM locales WHERE subdomain = %s LIMIT 1',
+				$locale
+			) );
+		} else if ( 5 === strlen( $locale ) ) {
+			$subdomain = $this->db->get_var( $this->db->prepare(
+				'SELECT subdomain FROM locales WHERE locale = %s LIMIT 1',
+				$locale
+			) );
+		}
+
+		wp_cache_set( $cache_key, $subdomain, $this->i18n_cache_group );
+
+		return $subdomain;
+	}
+}
\ No newline at end of file

Property changes on: extend/plugins-plugins/svn-track/class.dotorg-plugins-i18n.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: translate/bin/set-wp-plugin-project.php
===================================================================
--- translate/bin/set-wp-plugin-project.php	(revision 0)
+++ translate/bin/set-wp-plugin-project.php	(working copy)
@@ -0,0 +1,249 @@
+<?php
+/*
+ * Sets the required GlotPress subproject environment for a WordPress plugin hosted on
+ * EG: https://translate.wordpress.org/projects/wp-plugins/livejournal-importer/
+ * * dev, dev-readme[, stable, stable-readme]
+ * * starts with the same translation sets as existing on https://translate.wordpress.org/projects/wp-plugins/wordpress-importer/dev
+ */
+
+// Test script arguments.
+
+if ( $argc < 2 || !preg_match( '/^[^\/]+$/', $argv[1] ) ) {
+	echo "Usage: $argv[0] plugin-slug\n";
+	echo "Example:\n$argv[0] akismet\n";
+	exit( 1 );
+}
+
+/*
+ * Define environments, paths, supported languages.
+ */
+
+$master_project_path = 'wp-plugins';
+$plugin_slug         = trim( strtolower( $argv[1] ) );
+$plugin_project_path = "{$master_project_path}/{$plugin_slug}";
+
+/*
+ * Make sure we want to process that plugin, or exit on failure.
+ * TODO: remove when we go live for all plugins
+ */
+
+$valid_plugins = array(
+	'akismet', 'wpcat2tag-importer', 'wordpress-importer',
+	'utw-importer', 'textpattern-importer', 'stp-importer',
+	'rss-importer', 'opml-importer', 'movabletype-importer',
+	'livejournal-importer', 'greymatter-importer', 'dotclear-importer',
+	'blogware-importer', 'blogger-importer', 'tumblr-importer',
+	'bbpress', 'wordpress-beta-tester', ''
+);
+
+if ( !in_array( $plugin_slug, $valid_plugins ) ) {
+	echo "Sorry, but {$plugin_slug} is not a plugin we wish to process at the moment.\n";
+	exit( 1 );
+}
+
+/*
+ *  Load GP.
+ */
+
+include __DIR__ . '/../glotpress/gp-load.php';
+
+/*
+ * Get data for master parent project, or exit on failure.
+ */
+
+$master_project = GP::$project->by_path( $master_project_path );
+
+if ( empty( $master_project ) ) {
+	echo "Sorry, but couldn't find https://translate.wordpress.org/projects/{$master_project_path}.\n";
+	exit( 1 );
+}
+
+/*
+ * Get the plugin's code, and name from the latter, or exit on failure.
+ */
+
+$plugin_code_content = file_get_contents( "https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/{$plugin_slug}.php" );
+
+if ( empty( $plugin_code_content ) ) {
+	echo "Sorry, but couldn't find https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/{$plugin_slug}.php.\n";
+	exit( 1 );
+}
+
+$plugin_name = search_string( $plugin_code_content, '/Plugin Name: (.+)/' );
+
+/*
+ * Get or create the plugin GP project, or exit on failure
+ */
+
+$plugin_project = GP::$project->by_path( $plugin_project_path );
+
+if ( empty( $plugin_project ) ) {
+	$plugin_desc = search_string( $plugin_code_content, '/Description: (.+)/' );
+
+	$plugin_project = GP::$project->create( new GP_Project( array(
+		'name'              => $plugin_name,
+		'slug'              => $plugin_slug,
+		'parent_project_id' => $master_project->id,
+		'description'       => $plugin_desc,
+		'active'            => 1,
+	) ) );
+
+	if ( empty( $plugin_project ) ) {
+		echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$plugin_project_path}.\n";
+		exit( 1 );
+	}
+
+	echo "Created https://translate.wordpress.org/projects/{$plugin_project_path}.\n";
+}
+
+/*
+ * Define if this plugin will need both a dev and stable branch, or just the former.
+ * Exit if we can't find a readme file or declared stable tag (supports trunk as the latter).
+ */
+
+$plugin_readme_content = file_get_contents( "https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/readme.txt" );
+
+if ( empty( $plugin_readme_content ) ) {
+	echo "Sorry, but couldn't find https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/readme.txt.\n";
+	exit( 1 );
+}
+
+$stable_tag = search_string( $plugin_readme_content, '/Stable tag: (.+)/' );
+
+if ( empty( $stable_tag ) ) {
+	echo "Sorry, but couldn't find a stable tag in https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/readme.txt.\n";
+	exit( 1 );
+}
+
+$needs_stable = ( 'trunk' !== $stable_tag ) ? true : false;
+
+/*
+ * Deal with the always-existing dev and dev-readme branches.
+ */
+
+$plugin_dev_branches_projects = handle_plugin_project_branches( $plugin_project_path, $plugin_name, $plugin_project->id, false );
+
+/*
+ * Deal with the stable and stable-readme branches, if needed.
+ */
+
+if ( $needs_stable ) {
+	$plugin_stable_branches_projects = handle_plugin_project_branches( $plugin_project_path, $plugin_name, $plugin_project->id, true );
+}
+
+/*
+ * Deal with initial translation sets.
+ * Using https://translate.wordpress.org/projects/wp-plugins/wordpress-importer/dev as a template for defaults.
+ */
+
+$wp_importer_project = GP::$project->by_path( "{$master_project_path}/wordpress-importer/dev" );
+
+if ( ! empty( $wp_importer_project ) && !empty( $plugin_dev_branches_projects ) ) {
+	$translation_sets = (array) GP::$translation_set->by_project_id( $wp_importer_project->id );
+
+	if ( !empty( $translation_sets ) ) {
+		add_translation_sets_to_branches( $translation_sets, $plugin_dev_branches_projects );
+
+		if ( ! empty( $plugin_stable_branches_projects ) ) {
+			add_translation_sets_to_branches( $translation_sets, $plugin_stable_branches_projects );
+		}
+	}
+}
+
+/*
+ * We're done!
+ */
+echo "All set with https://translate.wordpress.org/projects/{$plugin_project_path}\n";
+exit( 0 );
+
+
+
+
+function search_string( $text, $pattern ) {
+	if ( empty( $text ) || empty( $pattern ) )
+		return '';
+	preg_match( $pattern, $text, $matches );
+	if ( empty( $matches ) || empty( $matches[1] ) || is_array( $matches[1] ) )
+		return '';
+	// Do clean out potential comment closing delimiter (*/) out of string
+	return preg_replace( '|^(.+)[\s]+?\*/$|', '\1', trim( $matches[1] ) );
+}
+
+function handle_plugin_project_branches( $plugin_project_path, $plugin_name, $parent_id, $for_stable ) {
+	if ( true === $for_stable ) {
+		$branch_slug = 'stable';
+	} else {
+		$branch_slug = 'dev';
+		$for_stable  = false;
+	}
+
+	$code_branch_project_path = "{$plugin_project_path}/{$branch_slug}";
+	$code_branch_project      = GP::$project->by_path( $code_branch_project_path );
+
+	if ( empty( $code_branch_project ) ) {
+		$code_branch_project = GP::$project->create( new GP_Project( array(
+			'name'              => ( $for_stable ) ? 'Stable (latest release)' : 'Development (trunk)',
+			'slug'              => $branch_slug,
+			'parent_project_id' => $parent_id,
+			'description'       => ( ( $for_stable ) ? 'Stable' : 'Development' ) . " version of the {$plugin_name} plugin.",
+			'active'            => 1,
+		) ) );
+
+		if ( empty( $code_branch_project ) ) {
+			echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$code_branch_project_path}.\n";
+		} else {
+			echo "Created https://translate.wordpress.org/projects/{$code_branch_project_path}.\n";
+		}
+	}
+
+	$readme_branch_project_path = "{$plugin_project_path}/{$branch_slug}-readme";
+	$readme_branch_project      = GP::$project->by_path( $readme_branch_project_path );
+
+	if ( empty( $readme_branch_project ) ) {
+		$readme_branch_project = GP::$project->create( new GP_Project( array(
+			'name'              => ( $for_stable ) ? 'Stable Readme (latest release)' : 'Development Readme (trunk)',
+			'slug'              => "{$branch_slug}-readme",
+			'parent_project_id' => $parent_id,
+			'description'       => ( ( $for_stable ) ? 'Stable' : 'Development' ) . " version of the {$plugin_name} plugin's readme.txt file.",
+			'active'            => 1,
+		) ) );
+
+		if ( empty( $readme_branch_project ) ) {
+			echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$readme_branch_project_path}.\n";
+		} else {
+			echo "Created https://translate.wordpress.org/projects/{$readme_branch_project_path}.\n";
+		}
+	}
+
+	return array( 'code' => $code_branch_project, 'readme' => $readme_branch_project );
+}
+
+function add_translation_sets_to_branches( $translation_sets, $branches_projects ) {
+	foreach( (array) $branches_projects as $project ) {
+		if ( empty( $project ) || empty( $project->id ) )
+			continue;
+
+		foreach ( $translation_sets as $ts ) {
+			if ( empty( $ts ) || empty( $ts->name ) )
+				continue;
+
+			$existing = GP::$translation_set->by_project_id_slug_and_locale( $project->id, $ts->slug, $ts->locale );
+
+			if ( !empty($existing) )
+				continue;
+
+			$new_ts = GP::$translation_set->create( array(
+				'project_id' => $project->id,
+				'name'       => $ts->name,
+				'locale'     => $ts->locale,
+				'slug'       => $ts->slug,
+			) );
+
+			if ( empty( $new_ts ) ) {
+				echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$project->path}/{$ts->locale}/{$ts->slug}.\n";
+			} else {
+				echo "Created https://translate.wordpress.org/projects/{$project->path}/{$ts->locale}/{$ts->slug}.\n";
+			}
+		}
+	}
+}
\ No newline at end of file

Property changes on: translate/bin/set-wp-plugin-project.php
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: extend/plugins-plugins/svn-track/class.dotorg-plugins-tracker.php
===================================================================
--- extend/plugins-plugins/svn-track/class.dotorg-plugins-tracker.php	(revision 9430)
+++ extend/plugins-plugins/svn-track/class.dotorg-plugins-tracker.php	(working copy)
@@ -84,6 +84,10 @@
 		add_action( 'bb_new_post',  array(&$this, 'bb_new_post'),  -1 );
 		add_action( 'bb_update_post',  array(&$this, 'bb_new_post'),  -1 );
 
+		add_filter( 'topic_title', array( &$this, 'translate_title' ) );
+		add_filter( 'topic_link',  array( &$this, 'translate_link' ) );
+		add_filter( 'search_link', array( &$this, 'translate_link' ) );
+
 		bb_register_view( 'new',     __( 'Newest', 'wporg' ),           array( 'order_by' => 'topic_start_time', 'sticky' => 'all', 'open' => 1 ) );
 		bb_register_view( 'updated', __( 'Recently Updated', 'wporg' ), array( 'order_by' => 'topic_time', 'sticky' => 'all', 'open' => 1 ) );
 		bb_register_view( 'popular', __( 'Most Popular', 'wporg' ),     array( 'meta_key' => 'downloads', 'order_by' => '0 + tm.meta_value', 'sticky' => 'all', 'open' => 1 ) );
@@ -93,13 +97,18 @@
 				bb_set_current_user( $user->ID );
 			}
 
-			if ( 1 < $argc && 'update' == $argv[1] ) {
+			if ( 1 < $argc && ( 'update' == $argv[1] || 'i18n' == $argv[1] ) ) {
 				if ( !isset( $_SERVER['REMOTE_ADDR'] ) )
 					$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
 
 				if ( 2 == $argc ) // php bb-load.php update
 					return $this->process_changes();
 
+				if ( 'i18n' == $argv[1] ) {
+					// php plugins/bb-load.php i18n slug [dev|stable, default: dev] [code|readme|all, default: all]
+					return $this->process_i18n( $argv[2], ( 'stable' === $argv[3] ) ? $argv[3] : 'dev', $argv[4] );
+				}
+
 				switch ( $argv[2] ) {
 				case 'all' :
 					return $this->process_all();
@@ -277,9 +286,45 @@
 			$content = preg_replace_callback( "#<img class='screenshot' src='(screenshot-[^']+)' alt='[^']+' />#", array( &$this, '_screenshot_img_tag' ), $content );
 		}
 
-		return $content;
+		return $this->translate( $show, $content );
 	}
 
+	function i18n_obj( $db, $tracker = null ) {
+		if ( !class_exists( 'Dotorg_Plugin_I18n' ) )
+			require_once( __DIR__ . '/class.dotorg-plugins-i18n.php' );
+		return new Dotorg_Plugin_I18n( $db, $tracker );
+	}
+
+	function translate( $key, $content, $args = array() ) {
+		return $this->i18n_obj( $this->db )->translate( $key, $content, $args );
+	}
+
+	/*
+	 * Used by:
+	 * * add_filter( 'topic_title', array( &$this, 'translate_title' ) );
+	 */
+	function translate_title( $string ) {
+		return $this->translate( 'title', $string );
+	}
+
+	/*
+	 * Converts a WP Plugins Directory link into the related localized link,
+	 * when on localized hosts (eg: fr.wordpress.org).
+	 *
+	 * Used by:
+	 * * add_filter( 'topic_link',  array( &$this, 'translate_link' ) );
+	 * * add_filter( 'search_link', array( &$this, 'translate_link' ) );
+	 * * self::session_url()
+	 */
+	function translate_link( $string ) {
+		$server_name = strtolower( $_SERVER['SERVER_NAME'] );
+		if ( false === strpos( $string, 'wordpress.org/plugins' )                      // passed string is uninteresting
+			|| preg_match( '|^(https?:)?//[^\.]{2}\.wordpress\.org(\/.*)?|', $string ) // passed string is already a localized wp.org host
+			|| !preg_match( '|^[^\.]{2}\.wordpress\.org$|', $server_name ) )           // server name not a localized wp.org host
+				return $string;
+		return str_replace( '//wordpress.org/plugins/', '//' . $server_name . '/plugins/', $string );
+	}
+
 	function _screenshot_shortcode( $matches ) {
 		return $this->_screenshot_image( $matches[1], $matches[2] );
 	}
@@ -1308,11 +1353,14 @@
 				$path_rels = array_merge( $path_rels, array_keys( $entry->paths ) );
 			}
 			$return_revision = array_shift( array_keys( $triggers ) );
-                } else { // its some paths
+		} else { // its some paths
 			$path_rels =& $triggers;
 			$return_revision = true;
 		}
 
+		// Reprocess code and/or readme i18n (method figures out if actually needed)
+		// $this->i18n_obj( $this->db, $this )->process_paths_array( $path_rels );
+
 		if ( ( isset($types['roots']) && $types['roots'] ) || ( isset($types['paths_in_roots']) && $types['paths_in_roots'] ) ) {
 			if ( 'grouped' === $types['roots'] ) // roots are pre-grouped
 				$roots =& $path_rels;
@@ -1702,6 +1750,10 @@
 			else
 				bb_delete_topicmeta( $topic_id, $key );
 		}
+
+		// Reprocess the readme for translation.
+		// DEBUG: placeholder, untested and not ready as of yet.
+		// $this->process_i18n( $slug, 'stable', 'readme' ); // Figures on its own if stable means dev (trunk).
 	}
 
 	function load_topic_page( $topic_id ) {
@@ -1871,12 +1923,12 @@
 			return 'http://wordpress.org/support/view/plugin-reviews/' . $topic->topic_slug;
 		if ( bb_get_option( 'mod_rewrite' ) ) {
 			if ( 'description' == $section )
-				return rtrim( get_topic_link( $topic_id ), '/' ) . '/';
-			return rtrim( get_topic_link( $topic_id ), '/' ) . "/$section/";
+				return $this->translate_link( rtrim( get_topic_link( $topic_id ), '/' ) . '/' );
+			return  $this->translate_link( rtrim( get_topic_link( $topic_id ), '/' ) . "/$section/" );
 		} else {
 			if ( 'description' == $section )
-				return remove_query_arg( 'show', get_topic_link( $topic_id ) );
-			return add_query_arg( 'show', $section, get_topic_link( $topic_id ) );
+				return  $this->translate_link( remove_query_arg( 'show', get_topic_link( $topic_id ) ) );
+			return  $this->translate_link( add_query_arg( 'show', $section, get_topic_link( $topic_id ) ) );
 		}
 	}
 
@@ -2142,6 +2194,10 @@
 		return false;
 	}
 
+	function process_i18n( $slug, $branch = 'dev', $type = 'all' ) {
+		return $this->i18n_obj( $this->db, $this )->process( $slug, $branch, $type );
+	}
+
 	function get_all_roots( $via = 'local' ) {
 		global $bbdb;
 		$root_rels = false;
Index: bb-theme/plugins/search-form.php
===================================================================
--- bb-theme/plugins/search-form.php	(revision 9430)
+++ bb-theme/plugins/search-form.php	(working copy)
@@ -1,4 +1,4 @@
-<form action="<?php bb_uri( 'search.php', null, BB_URI_CONTEXT_FORM_ACTION ); ?>" method="get" id="plugins-search">
+<form action="<?php echo apply_filters( 'search_link', bb_get_uri( 'search.php', null, BB_URI_CONTEXT_FORM_ACTION ) ); ?>" method="get" id="plugins-search">
 <p>
 	<input type="text" class="text" maxlength="100" name="q" value="<?php echo attribute_escape( $q ); ?>" />
 	<input type="submit" value="<?php echo attribute_escape( __( 'Search Plugins', 'wporg' ) ); ?>" class="button" />
Index: bb-theme/plugins/sub.php
===================================================================
--- bb-theme/plugins/sub.php	(revision 9430)
+++ bb-theme/plugins/sub.php	(working copy)
@@ -42,7 +42,7 @@
 <?php if ( ! is_front() && ! is_bb_search() ) : ?>
 	<h4><?php _e( 'Search Plugins', 'wporg' ); ?></h4>
 
-	<form id="side-search" method="get" action="//wordpress.org/plugins/search.php">
+	<form id="side-search" method="get" action="<?php echo apply_filters( 'search_link', '//wordpress.org/plugins/search.php' ); ?>">
 		<div>
 			<input type="text" class="text" name="q" value="<?php echo esc_attr( empty( $_REQUEST['q'] ) ? "" : attribute_escape( $_REQUEST['q'] ) ); ?>" />
 			<input type="submit" class="button" value="Search" /><br class="clear" />
Index: bb-theme/themes/search-form.php
===================================================================
--- bb-theme/themes/search-form.php	(revision 9430)
+++ bb-theme/themes/search-form.php	(working copy)
@@ -1,4 +1,4 @@
-<form action="<?php bb_uri('search.php', null, BB_URI_CONTEXT_FORM_ACTION); ?>" method="get" id="plugins-search">
+<form action="<?php echo apply_filters( 'search_link', bb_get_uri( 'search.php', null, BB_URI_CONTEXT_FORM_ACTION ) ); ?>" method="get" id="plugins-search">
 <p>
 	<input type="text" class="text" maxlength="100" name="q" value="<?php echo attribute_escape( $q ); ?>" />
 	<input type="submit" value="<?php echo attribute_escape( __( 'Search Themes', 'wporg' ) ); ?>" class="button" /></p>
Index: bb-theme/wporg/plugin-reviews.php
===================================================================
--- bb-theme/wporg/plugin-reviews.php	(revision 9430)
+++ bb-theme/wporg/plugin-reviews.php	(working copy)
@@ -59,7 +59,7 @@
 				<li><a href='/extend/kvetch/'>Kvetch!</a></li>
 			</ul>
 
-<h4>Search Plugins</h4><form id='side-search' method='get' action='//wordpress.org/plugins/search.php'>
+<h4>Search Plugins</h4><form id='side-search' method='get' action='<?php echo apply_filters( 'topic_link', '//wordpress.org/plugins/search.php' ); ?>'>
 <div>
 	<input type='text' class='text' name='q' value='' />
 	<input type='submit' class='button' value='Search' /><br class='clear' />
Index: bb-theme/wporg/plugin.php
===================================================================
--- bb-theme/wporg/plugin.php	(revision 9430)
+++ bb-theme/wporg/plugin.php	(working copy)
@@ -52,7 +52,7 @@
 				<li><a href='/extend/kvetch/'>Kvetch!</a></li>
 			</ul>
 
-<h4>Search Plugins</h4><form id='side-search' method='get' action='//wordpress.org/plugins/search.php'>
+<h4>Search Plugins</h4><form id='side-search' method='get' action='<?php echo apply_filters( 'topic_link', '//wordpress.org/plugins/search.php' ); ?>'>
 <div>
 	<input type='text' class='text' name='q' value='' />
 	<input type='submit' class='button' value='Search' /><br class='clear' />
