Index: trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-i18n.php
===================================================================
--- trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-i18n.php	(revision 3299)
+++ trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-i18n.php	(working copy)
@@ -9,7 +9,7 @@
 class i18n {
 
 	/**
-	 * Translate a Term Name.
+	 * Translates a term name.
 	 *
 	 * @param \WP_Term $term The Term object to translate.
 	 * @return \WP_Term The term object with a translated `name` field.
@@ -27,6 +27,134 @@
 	}
 
 	/**
+	 * Retrieves the translated version of a plugin.
+	 *
+	 * @param int $post_id The post ID of a plugin.
+	 * @return \WP_Post|null WP_Post object on success, null on failure.
+	 */
+	static function get_translated_plugin_post( $post_id, $locale = null ) {
+		if ( null === $locale ) {
+			$locale = get_locale();
+		}
+
+		if ( ! $locale || 'en_US' === $locale ) {
+			return null;
+		}
+
+		// @todo Do we need caching or is advanced post cache enough?
+		$posts = get_posts( array(
+			'post_type'   => 'plugin_translated',
+			'name'        => $locale,
+			'post_parent' => $post_id,
+			'post_status' => array( 'publish' ),
+			'numberposts' => 1,
+		) );
+
+		if ( ! $posts ) {
+			return null;
+		}
+
+		$plugin = reset( $posts );
+		return $plugin;
+	}
+
+	/**
+	 * Translates the title of a plugin.
+	 *
+	 * @param string $title   The post title.
+	 * @param int    $post_id The post ID.
+	 * @return string Filtered title.
+	 */
+	static function translate_title( $title, $post_id ) {
+		$post = get_post( $post_id );
+		if ( ! $post || 'plugin' !== $post->post_type ) {
+			return $title;
+		}
+
+		$translated_plugin = self::get_translated_plugin_post( $post->ID );
+		if ( ! $translated_plugin || ! $translated_plugin->post_title ) {
+			return $title;
+		}
+
+		return $translated_plugin->post_title;
+	}
+
+	/**
+	 * Translates the title of a plugin.
+	 *
+	 * @param string $content The post content.
+	 * @return string Filtered content.
+	 */
+	static function translate_content( $content ) {
+		$post = get_post();
+		if ( ! $post || 'plugin' !== $post->post_type ) {
+			return $content;
+		}
+
+		$translated_plugin = self::get_translated_plugin_post( $post->ID );
+		if ( ! $translated_plugin || ! $translated_plugin->post_content ) {
+			return $content;
+		}
+
+		return $translated_plugin->post_content;
+	}
+
+	/**
+	 * Translates the excerpt of a plugin.
+	 *
+	 * @param string $excerpt The post excerpt.
+	 * @param int    $post_id The post ID.
+	 * @return string Filtered excerpt.
+	 */
+	static function translate_excerpt( $excerpt, $post_id ) {
+		$post = get_post( $post_id );
+		if ( ! $post || 'plugin' !== $post->post_type ) {
+			return $excerpt;
+		}
+
+		$translated_plugin = self::get_translated_plugin_post( $post->ID );
+		if ( ! $translated_plugin || ! $translated_plugin->post_excerpt ) {
+			return $excerpt;
+		}
+
+		return $translated_plugin->post_excerpt;
+	}
+
+	/**
+	 * Translates the screenshot descriptions of a plugin.
+	 *
+	 * @param null|array|string $value     The value get_metadata() should return - a single metadata value,
+	 *                                     or an array of values.
+	 * @param int               $object_id Object ID.
+	 * @param string            $meta_key  Meta key.
+	 * @param bool              $single    Whether to return only the first value of the specified $meta_key.
+	 * @return mixed Will be an array if $single is false. Will be value of meta data
+	 *               field if $single is true.
+	 */
+	static function translate_screenshot_descriptions( $value, $object_id, $meta_key, $single ) {
+		if ( 'screenshots' !== $meta_key ) {
+			return $value;
+		}
+
+		$post = get_post( $object_id );
+		if ( ! $post || 'plugin' !== $post->post_type ) {
+			return $value;
+		}
+
+		$translated_plugin = self::get_translated_plugin_post( $post->ID );
+		if ( ! $translated_plugin ) {
+			return $value;
+		}
+
+		$translated_value = get_post_meta( $translated_plugin->ID, 'screenshots', false );
+		if ( ! $translated_value ) {
+			return $value;
+		}
+
+		return $translated_value;
+	}
+
+	/**
 	 * A private method to hold a list of the strings contained within the Database.
 	 *
 	 * This function is never called, and only exists so that out pot tools can detect the strings.
Index: trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php
===================================================================
--- trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php	(revision 3299)
+++ trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php	(working copy)
@@ -91,9 +91,17 @@
 				'read_private_posts' => 'do_not_allow',
 				'delete_posts'       => 'do_not_allow',
 				'create_posts'       => 'do_not_allow',
-			)
+			),
 		) );
 
+		register_post_type( 'plugin_translated', array(
+			'public'       => false,
+			'rewrite'      => false,
+			'query_var'    => false,
+			'can_export'   => false,
+			'hierarchical' => true,
+		) );
+
 		register_taxonomy( 'plugin_section', 'plugin', array(
 			'hierarchical'      => true,
 			'query_var'         => 'plugin_section',
@@ -216,6 +224,10 @@
 
 		if ( 'en_US' != get_locale() ) {
 			add_filter( 'get_term', array( __NAMESPACE__ . '\i18n', 'translate_term' ) );
+			add_filter( 'the_title', array( __NAMESPACE__ . '\i18n', 'translate_title' ), 10, 2 );
+			add_filter( 'wporg_plugins_content', array( __NAMESPACE__ . '\i18n', 'translate_content' ) ); // @todo get_the_content
+			add_filter( 'get_the_excerpt', array( __NAMESPACE__ . '\i18n', 'translate_excerpt', 10, 2 ) );
+			add_filter( 'get_post_metadata', array( __NAMESPACE__ . '\i18n', 'translate_screenshot_descriptions' ), 10, 4 );
 		}
 
 		// Instantiate our copy of the Jetpack_Search class.
Index: trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php
===================================================================
--- trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php	(revision 3299)
+++ trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins/template-parts/plugin-single.php	(working copy)
@@ -11,7 +11,8 @@
 use WordPressdotorg\Plugin_Directory\Plugin_Directory;
 use WordPressdotorg\Plugin_Directory\Template;
 
-$content = call_user_func( array( Plugin_Directory::instance(), 'split_post_content_into_pages' ), get_the_content() );
+$content = apply_filters( 'wporg_plugins_content', get_the_content() );
+$content = call_user_func( array( Plugin_Directory::instance(), 'split_post_content_into_pages' ), $content );
 
 $widget_args = array(
 	'before_title' => '<h4 class="widget-title">',
@@ -59,4 +60,4 @@
 		?>
 
 	</div><!-- .entry-meta -->
-</article><!-- #post-## -->
\ No newline at end of file
+</article><!-- #post-## -->
