Making WordPress.org

Ticket #760: 760-wporg.5.diff

File 760-wporg.5.diff, 40.5 KB (added by stephdau, 10 years ago)

Production-ready version

  • extend/plugins-plugins/svn-track/class.dotorg-plugins-i18n.php

     
     1<?php
     2class Dotorg_Plugin_I18n {
     3        var $db;              // Set in __construct()
     4        var $tracker;         // Set in __construct()
     5        var $master_project   = 'wp-plugins';
     6        var $i18n_cache_group = 'plugins-i18n';
     7
     8        // TODO: remove when we launch for all plugins.
     9        var $translated_plugins = array(
     10                'akismet', 'wpcat2tag-importer', 'wordpress-importer',
     11                'utw-importer', 'textpattern-importer', 'stp-importer',
     12                'rss-importer', 'opml-importer', 'movabletype-importer',
     13                'livejournal-importer', 'greymatter-importer', 'dotclear-importer',
     14                'blogware-importer', 'blogger-importer', 'tumblr-importer',
     15                'bbpress', 'wordpress-beta-tester', 'theme-check'
     16        );
     17
     18        function __construct( $db, $tracker = null ) {
     19                if ( !empty( $db ) && is_object( $db ) )
     20                        $this->db = $db;
     21                if ( !empty( $tracker ) && is_object( $tracker ) )
     22                        $this->tracker = $tracker;
     23                wp_cache_add_global_groups( $this->i18n_cache_group );
     24        }
     25
     26        /*
     27         * ***********************
     28         * Processing
     29         * ***********************
     30         */
     31
     32        function process( $slug, $branch = 'dev', $type = 'all' ) {
     33                $slug = trim( $slug, '/' );
     34
     35                if ( empty( $slug ) || false !== strpos( $slug, '/' ) || empty( $this->tracker ) )
     36                        return false;
     37
     38                // DEBUG: in_array check is because we'll start the program with a finite list of plugins
     39                // TODO: remove when we launch for all plugins.
     40                if ( ! in_array( $slug, $this->translated_plugins ) )
     41                        return false;
     42
     43                if ( 'stable' !== $branch )
     44                        $branch = 'dev';
     45
     46                if ( 'code' !== $type && 'readme' !== $type )
     47                        $type = 'all';
     48
     49                $path_rel = "{$slug}/trunk/";
     50
     51                if ( 'stable' === $branch ) {
     52                        if ( false == ( $stable_tag = $this->tracker->get_stable_tag_dir_using( $path_rel ) ) ) {
     53                                // Can't get a stable tag, bail out
     54                                return false;
     55                        } else if ( 'trunk' == trim( $stable_tag['tag_dir'], '/' ) ) {
     56                                // If stable is trunk, then it's really same as dev, switch to that
     57                                $branch = 'dev';
     58                        } else {
     59                                // We're dealing with an actual stable tag, go for it
     60                                $path_rel = "{$slug}/{$stable_tag['tag_dir']}"; //
     61                        }
     62                }
     63
     64                // Ensure that GlotPress is all set for the plugin
     65                $this->set_glotpress_for_plugin( $slug );
     66
     67                if ( 'code' === $type || 'all' === $type )
     68                        $this->process_code( $path_rel, $branch );
     69
     70                if ( 'readme' === $type || 'all' === $type )
     71                        $this->process_readme( $path_rel, $branch );
     72
     73                echo "Processed {$type} for {$path_rel}\n";
     74                return true;
     75        }
     76
     77        function process_code( $path_rel, $branch = 'dev' ) {
     78                if ( empty( $this->tracker ) )
     79                        return false;
     80
     81                $slug = preg_replace( '|^/?([^/]+)/?.+?$|', '\1', $path_rel );
     82
     83                if ( empty( $slug ) || !preg_match( '/^[a-z0-9-]+$/i', $slug ) )
     84                        return false;
     85
     86                // DEBUG: in_array check is because we'll start the program with a finite list of plugins
     87                // TODO: remove when we launch for all plugins.
     88                if ( !in_array( $slug, $this->translated_plugins ) )
     89                        return false;
     90
     91                $export_path = $this->tracker->create_export( $path_rel );
     92
     93                if ( empty( $export_path ) || !is_dir( $export_path ) )
     94                        return false;
     95
     96                $old_cwd = getcwd();
     97                chdir( $export_path );
     98
     99                // Check for a plugin text domain declaration and loading, grep recursively, not necessarily in [slug].php
     100                if ( ! shell_exec( 'grep -r --include "*.php" "Text Domain: ' . escapeshellarg( $slug ) . '" .' ) && ! shell_exec( 'grep -r --include "*.php" "\bload_plugin_textdomain\b" .' ) )
     101                        return false;
     102
     103                if ( !class_exists( 'PotExtMeta' ) )
     104                        require_once( __DIR__ . '/i18n-tools/pot-ext-meta.php' );
     105
     106                // Create pot file from code
     107                $pot_file = "./tmp-{$slug}.pot"; // Using tmp- prefix in case a plugin has $slug.pot committed
     108                $makepot  = new MakePOT;
     109
     110                if ( ! $makepot->wp_plugin( '.', $pot_file, $slug ) || ! file_exists( $pot_file ) )
     111                        return false;
     112
     113                // DEBUG
     114                // system( "cat {$pot_file}" );
     115
     116                $this->import_to_glotpress_project( $slug, $branch, $pot_file );
     117
     118                chdir( $old_cwd );
     119                return true;
     120        }
     121
     122        function process_readme( $path_rel, $branch = 'dev' ) {
     123                if ( empty( $this->tracker ) )
     124                        return false;
     125
     126                $slug = preg_replace( '|^/?([^/]+)/?.+?$|', '\1', $path_rel );
     127
     128                if ( empty( $slug ) || !preg_match( '/^[a-z0-9-]+$/i', $slug ) )
     129                        return false;
     130
     131                // DEBUG: in_array as separate check because we'll start the program with a finite list of plugins
     132                // TODO: remove when we launch for all plugins.
     133                if ( !in_array( $slug, $this->translated_plugins ) )
     134                        return false;
     135
     136                $export_path = $this->tracker->create_export( $path_rel );
     137
     138                if ( empty( $export_path ) || !is_dir( $export_path ) )
     139                        return false;
     140
     141                $old_cwd = getcwd();
     142                chdir( $export_path );
     143
     144                $readme = $this->tracker->parse_readme_in( $path_rel );
     145
     146                $str_priorities = array();
     147
     148                if ( !class_exists( 'PO' ) )
     149                        require_once( __DIR__ . '/i18n-tools/pomo/po.php' );
     150
     151                $pot = new PO;
     152
     153                // No need for license, being in the directory implies GPLv2 or later. Add here otherwise.
     154                foreach ( array( 'name', 'short_description' ) as $key ) {
     155                        $readme[ $key ] = trim( $readme[ $key ] ) ;
     156                }
     157
     158                // If empty or "sketchy", get the plugin name form the PHP file's headers
     159                if ( empty( $readme['name'] ) || 'Plugin Name' == trim( $readme['name'] ) ) {
     160                        // -o in grep will make sure we don't get comments opening delimiters (//, /*) or spaces as part of string
     161                        $name_from_php  = trim( shell_exec( 'grep -o "\bPlugin Name:.*" ' . escapeshellarg( $slug ) . '.php' ) );
     162                        // Remove the header label
     163                        $name_from_php  = str_replace( 'Plugin Name:', '', $name_from_php );
     164                        // Do clean out potential comment closing delimiter (*/) out of string
     165                        $name_from_php  = preg_replace( '|^(.+)[\s]+?\*/$|', '\1', $name_from_php );
     166                        // Use trimmed results as plugin name
     167                        $readme['name'] = trim( $name_from_php );
     168                }
     169
     170                if ( !empty( $readme['name'] ) ) {
     171                        $pot->add_entry( new Translation_Entry ( array(
     172                                'singular'           => $readme['name'],
     173                                'extracted_comments' => 'Name.',
     174                        ) ) );
     175
     176                        $str_priorities[ $readme['name'] ] = 1;
     177                }
     178
     179                if ( !empty( $readme['short_description'] ) ) {
     180                        $pot->add_entry( new Translation_Entry ( array(
     181                                'singular'           => $readme['short_description'],
     182                                'extracted_comments' => 'Short description.',
     183                        ) ) );
     184
     185                        $str_priorities[ $readme['short_description'] ] = 1;
     186                }
     187
     188                if ( !empty( $readme['screenshots'] ) ) {
     189                        foreach ( $readme['screenshots'] as $sshot_key => $sshot_desc ) {
     190                                $sshot_desc = trim( $sshot_desc );
     191                                $pot->add_entry( new Translation_Entry ( array(
     192                                        'singular'           => $sshot_desc,
     193                                        'extracted_comments' => 'Screenshot description.',
     194                                ) ) );
     195                        }
     196
     197                }
     198
     199                if ( empty( $readme['sections'] ) )
     200                        $readme['sections'] = array();
     201
     202                // Adding remaining content as a section so it's processed by the same loop below
     203                if ( !empty( $readme['remaining_content'] ) )
     204                        $readme['sections']['remaining_content'] = $readme['remaining_content'];
     205
     206                $strings = array();
     207
     208                foreach ( $readme['sections'] as $section_key => $section_text ) {
     209                        if ( 'screenshots' == $section_key )
     210                                continue;
     211
     212                        /*
     213                         * Scanned tags based on block elements found in Automattic_Readme::filter_text() $allowed.
     214                         * Scanning H3/4, li, p and blockquote. Other tags are ignored  in strings (a, strong, cite, etc).
     215                         * Processing notes:
     216                         * * Don't normalize/modify original text, will be used as a search pattern in original doc in some use-cases.
     217                         * * Using regexes over XML parsing for performance reasons, could move to the latter for more accuracy.
     218                         */
     219
     220                        if ( 'changelog' !== $section_key ) { // No need to scan non-translatable version headers in changelog
     221                                if ( preg_match_all( '|<h[3-4]+[^>]*>(.+)</h[3-4]+>|', $section_text, $matches ) ) {
     222                                        if ( !empty( $matches[1] ) ) {
     223                                                foreach ( $matches[1] as $text ) {
     224                                                        $strings = $this->handle_translator_comment( $strings, $text, "{$section_key} header" );
     225                                                }
     226                                        }
     227                                }
     228                        }
     229
     230                        if ( preg_match_all( '|<li>(.+)</li>|', $section_text, $matches ) ) {
     231                                if ( !empty( $matches[1] ) ) {
     232                                        foreach ( $matches[1] as $text ) {
     233                                                $strings = $this->handle_translator_comment( $strings, $text, "{$section_key} list item" );
     234                                                if ( 'changelog' === $section_key )
     235                                                        $str_priorities[ $text ] = -1;
     236                                        }
     237                                }
     238                        }
     239
     240                        if ( preg_match_all( '|<p>(.+)</p>|', $section_text, $matches ) ) {
     241                                if ( !empty( $matches[1] ) ) {
     242                                        foreach ( $matches[1] as $text ) {
     243                                                $strings = $this->handle_translator_comment( $strings, $text, "{$section_key} paragraph" );
     244                                                if ( 'changelog' === $section_key )
     245                                                        $str_priorities[ $text ] = -1;
     246                                        }
     247                                }
     248                        }
     249
     250                        if ( preg_match_all( '|<blockquote>(.+)</blockquote>|', $section_text, $matches ) ) {
     251                                if ( !empty( $matches[1] ) ) {
     252                                        foreach ( $matches[1] as $text ) {
     253                                                $strings = $this->handle_translator_comment( $strings, $text, "{$section_key} block quote" );
     254                                                if ( 'changelog' === $section_key )
     255                                                        $str_priorities[ $text ] = -1;
     256                                        }
     257                                }
     258                        }
     259                }
     260
     261                foreach ( $strings as $text => $comments ) {
     262                        $pot->add_entry( new Translation_Entry ( array(
     263                                'singular'           => $text,
     264                                'extracted_comments' => 'Found in ' . implode( $comments, ", " ) . '.',
     265                        ) ) );
     266                }
     267
     268                $pot_file = "./tmp-{$slug}-readme.pot";
     269                $pot->export_to_file( $pot_file );
     270
     271                // DEBUG
     272                // system( "cat {$pot_file}" );
     273
     274                // import to GlotPress, dev or stable
     275                $this->import_to_glotpress_project( $slug, "{$branch}-readme", $pot_file, $str_priorities );
     276
     277                chdir( $old_cwd );
     278                return true;
     279        }
     280
     281        function handle_translator_comment( $array, $key, $val ) {
     282                $val = trim( preg_replace( '/[^a-z0-9]/i', ' ', $val ) ); // cleanup key names for display
     283                if ( empty( $array[ $key ] ) ) {
     284                        $array[ $key ] = array( $val );
     285                } else if ( !in_array( $val, $array[ $key ] ) ) {
     286                        $array[ $key ][] = $val;
     287                }
     288                return $array;
     289        }
     290
     291        function import_to_glotpress_project( $project, $branch, $file, $str_priorities = array() ) {
     292                if ( empty( $project ) || empty( $branch ) || empty( $file ) )
     293                        return;
     294                // Note: this will only work if the GlotPress project/sub-projects exist.
     295                $cmd = 'php ' . __DIR__ . '/../../../translate/glotpress/scripts/import-originals.php -o po -p ' . escapeshellarg( "wp-plugins/{$project }/{$branch}" ) . ' -f ' . escapeshellarg( $file );
     296                // DEBUG
     297                // var_dump( $cmd );
     298                system( $cmd );
     299                if ( empty( $str_priorities ) )
     300                        return;
     301                $branch_id = $this->get_gp_branch_id( $project, $branch );
     302                // Set the string priorities in GP once the originals have been imported
     303                if ( empty( $branch_id ) )
     304                        return;
     305                foreach ( (array) $str_priorities as $str => $prio ) {
     306                        if ( 1 !== $prio && -1 !== $prio )
     307                                $prio = 0;
     308                        $res = $this->db->query( $this->db->prepare(
     309                                'UPDATE translate_originals SET priority = %d WHERE project_id = %d AND status = %s AND singular = %s',
     310                                $prio, $branch_id, '+active', $str
     311                        ) );
     312                }
     313        }
     314
     315        function set_glotpress_for_plugin( $plugin_slug ) {
     316                if ( empty( $plugin_slug ) )
     317                        return;
     318                $cmd = 'php ' . __DIR__ . '/../../../translate/bin/set-wp-plugin-project.php ' . escapeshellarg( $plugin_slug );
     319                // DEBUG
     320                // var_dump( $cmd );
     321                system( $cmd );
     322        }
     323
     324        function process_paths_array( $paths ) {
     325                if ( empty( $paths ) || !is_array( $paths ) )
     326                        return;
     327
     328                $to_process = array();
     329
     330                // Note: never assume all passed paths are for the same plugin or branch (dev vs stable)
     331                foreach( $paths as $path ) {
     332                        // Start with the quickest tests
     333                        if ( !preg_match( '/^[^\/]+\/(trunk|tags)\/.*$/', $path ) )
     334                                continue; // affected path doesn't call for reprocessing
     335
     336                        if ( preg_match( '/\/readme\.txt$/', $path ) ) {
     337                                $type = 'readme';
     338                        } else if ( preg_match( '/\.php$/', $path ) ) {
     339                                $type = 'code';
     340                        } else if ( preg_match( '/\/$/', $path ) || preg_match( '/\/[^\.]+$/', $path ) ) {
     341                                // Dealing with a directory (or extension-less file)
     342                                if ( preg_match( '/^[^\/]+\/(trunk|tags\/[^\/]+)\/?$/', $path ) ) {
     343                                        // Top level trunk or tagged release directory, process both code and readme
     344                                        $type = 'all';
     345                                } else {
     346                                        // Some other subdirectory that could contain code, only process the latter
     347                                        $type = 'code';
     348                                }
     349                        } else {
     350                                continue; // affected path doesn't call for reprocessing
     351                        }
     352
     353                        $plugin = preg_replace( '/^([^\/]+)(\/.*)?$/', '\1', $path );
     354
     355                        if ( empty( $plugin ) )
     356                                continue;
     357
     358                        // Finish with branch definition/test, since potentially the slowest, through $this->tracker->get_stable_tag_dir_using()
     359                        if ( false !== strpos( $path, "{$plugin}/trunk/" ) ) {
     360                                $branch = 'dev';
     361                        } else {
     362                                // Only process non-trunk revs if they are part of the latest stable tag
     363                                $latest_stable_tag = $this->tracker->get_stable_tag_dir_using( $plugin );
     364                                $expected_tag_path = "{$plugin}/tags/{$latest_stable_tag}";
     365                                if ( substr( $path, 0, strlen( $expected_tag_path ) ) === $expected_tag_path ) {
     366                                        $branch = 'stable';
     367                                }
     368                        }
     369
     370                        if ( empty( $branch ) )
     371                                continue; // affected path doesn't call for reprocessing
     372
     373                        // Using array keys so we only end up processing every combo once in a log
     374                        $to_process[ $plugin ][ $branch ][ $type ] = true;
     375                }
     376
     377                if ( empty( $to_process ) )
     378                        return;
     379
     380                // Now, process the plugin + branch + type combos, only once each
     381                foreach( $to_process as $plugin => $branches ) {
     382                        foreach( $branches as $branch => $types ) {
     383                                // Consolidate types into "all" if requesting both code and readme
     384                                if ( isset( $types[ 'code' ] ) && isset( $types[ 'readme' ] ) ) {
     385                                        $types[ 'all' ] = true;
     386                                }
     387                                // Late cleanup, to once again insure minimal reprocessing
     388                                if ( isset( $types[ 'all' ] ) ) {
     389                                        unset( $types[ 'code' ] );
     390                                        unset( $types[ 'readme' ] );
     391                                }
     392                                // Go for processing
     393                                foreach( $types as $type => $unused ) {
     394                                        $this->process( $plugin, $branch, $type );
     395                                }
     396                        }
     397                }
     398        }
     399
     400        /*
     401         * ***********************
     402         * Rendering
     403         * ***********************
     404         */
     405
     406        function translate( $key, $content, $args = array() ) {
     407                if ( empty( $key ) || empty( $content ) )
     408                        return $content;
     409
     410                if ( !empty( $args['topic_id'] ) && is_numeric( $args['topic_id'] ) )
     411                        $topic = get_topic( $args['topic_id'] );
     412                else
     413                        global $topic;
     414
     415                if ( empty( $topic ) )
     416                        return $content;
     417
     418                $language     = '';
     419                $server_name  = strtolower( $_SERVER['SERVER_NAME'] );
     420                if ( 'api.wordpress.org' == $server_name ) {
     421                        if ( preg_match( '/^[a-z]{2}(_[A-Z]{2})?$/', trim( $_REQUEST['locale'] ) ) ) {
     422                                $language = substr( trim( $_REQUEST['locale'] ), 0, 5 );
     423                        } else if ( !empty( $_REQUEST['request'] ) ) {
     424                                $request = maybe_unserialize( $_REQUEST['request'] );
     425                                if ( !empty( $request ) && !empty( $request->locale ) && preg_match( '/^[a-z]{2}(_[A-Z]{2})?$/', trim( $request->locale ) )  ) {
     426                                        $language = trim( $request->locale );
     427                                }
     428                        }
     429                } else if ( preg_match( '/^([^\.]+)\.wordpress\.org$/', $server_name, $matches ) ) {
     430                        $subdomain = $this->verify_subdomain( $matches[1] );
     431                        if ( ! empty( $subdomain ) )
     432                                $language = substr( $server_name, 0, 2 );
     433                }
     434
     435                if ( empty( $language ) || 'en' === $language || 'en_US' === $language )
     436                        return $content;
     437
     438                $slug = $topic->plugin_san;
     439
     440                // DEBUG: in_array check is because we'll start the program with a finite list of plugins
     441                // TODO: remove when we launch for all plugins.
     442                if ( empty( $slug ) || ! in_array( $slug, $this->translated_plugins ) )
     443                        return $content;
     444
     445                $branch = ( empty( $topic->stable_tag ) || 'trunk' === $topic->stable_tag ) ? 'dev' : 'stable';
     446
     447                if ( empty( $args['code_i18n'] ) || true !== $args['code_i18n'] )
     448                        $branch .= '-readme';
     449
     450                $cache_suffix = "{$language}:{$key}";
     451
     452                // Try the cache
     453                if ( false !== ( $cache = $this->cache_get( $slug, $branch, $cache_suffix ) ) ) {
     454                        // DEBUG
     455                        // var_dump( array( $slug, $branch, $cache_suffix, $cache ) );
     456                        return $cache;
     457                }
     458
     459                $originals = $this->get_gp_originals( $slug, $branch, $key, $content );
     460
     461                if ( empty( $originals ) )
     462                        return $content;
     463
     464                $translation_set_id = $this->get_gp_translation_set_id( $slug, $branch, $language );
     465
     466                if ( empty( $translation_set_id ) )
     467                        return $content;
     468
     469                foreach ( $originals as $original ) {
     470                        if ( empty( $original->id ) )
     471                                continue;
     472
     473                        $translation = $this->db->get_var( $this->db->prepare(
     474                                'SELECT translation_0 FROM translate_translations WHERE original_id = %d AND translation_set_id = %d AND status = %s',
     475                                $original->id, $translation_set_id, 'current'
     476                        ) );
     477
     478                        if ( empty( $translation ) )
     479                                continue;
     480
     481                        $content = $this->translate_gp_original( $original->singular, $translation, $content );
     482                }
     483
     484                $this->cache_set( $slug, $branch, $content, $cache_suffix );
     485
     486                return $content;
     487        }
     488
     489        function cache_key( $slug, $branch, $suffix = null ) {
     490                // EG keys
     491                // plugin:press-this:stable-readme:originals
     492                // plugin:press-this:stable-readme:original:title
     493                // plugin:press-this:stable-readme:fr:title
     494                $key = "{$this->master_project}:{$slug}:{$branch}";
     495                if ( !empty( $suffix ) )
     496                        $key .= ":{$suffix}";
     497                return $key;
     498        }
     499
     500        function cache_get( $slug, $branch, $suffix = null ) {
     501                $key = $this->cache_key( $slug, $branch, $suffix );
     502                // DEBUG
     503                // wp_cache_delete( $key, $this->i18n_cache_group );
     504                return wp_cache_get( $key, $this->i18n_cache_group );
     505        }
     506
     507        function cache_set( $slug, $branch, $content, $suffix = null ) {
     508                $key = $this->cache_key( $slug, $branch, $suffix );
     509                return wp_cache_set( $key, $content, $this->i18n_cache_group );
     510        }
     511
     512        function get_gp_branch_id( $slug, $branch ) {
     513                $cache_suffix = "branch_id";
     514
     515                if ( false !== ( $branch_id = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
     516                        return $branch_id;
     517
     518                $branch_id = $this->db->get_var( $this->db->prepare(
     519                        'SELECT id FROM translate_projects WHERE path = %s',
     520                        "wp-plugins/{$slug}/{$branch}"
     521                ) );
     522
     523                if ( empty( $branch_id ) )
     524                        $branch_id = 0;
     525
     526                $this->cache_set( $slug, $branch, $branch_id, $cache_suffix );
     527
     528                return $branch_id;
     529        }
     530
     531        function get_gp_originals( $slug, $branch, $key, $str ) {
     532                // Try to get a single original with the whole content first (title, etc), if passed, or get them all otherwise.
     533                if ( !empty( $key ) && !empty( $str ) ) {
     534                        $originals = $this->search_gp_original( $slug, $branch, $key, $str );
     535                        if ( !empty( $originals ) )
     536                                return array( $originals );
     537                        // Do not cache this as originals, search_gp_original() does its own caching
     538                }
     539
     540                $cache_suffix = 'originals';
     541
     542                if ( false !== ( $originals = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
     543                        return $originals;
     544
     545                $branch_id = $this->get_gp_branch_id( $slug, $branch );
     546
     547                if ( empty( $branch_id ) )
     548                        return array();
     549
     550                $originals = $this->db->get_results( $this->db->prepare(
     551                        'SELECT id, singular, comment FROM translate_originals WHERE project_id = %d AND status = %s',
     552                        $branch_id, '+active'
     553                ) );
     554
     555                if ( empty( $originals ) )
     556                        $originals = array(); // still cache if empty, but as array, never false
     557
     558                $this->cache_set( $slug, $branch, $originals, $cache_suffix );
     559
     560                return $originals;
     561        }
     562
     563        function get_gp_translation_set_id( $slug, $branch, $locale ) {
     564                $locale = strtolower( $locale );
     565
     566                if ( false !== strpos( $locale, '_' ) ) {
     567                        $locale = str_replace( '_', '-', $locale );
     568                }
     569
     570                $cache_suffix = "{$locale}:translation_set_id";
     571
     572                if ( false !== ( $translation_set_id = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
     573                        return $translation_set_id;
     574
     575                $branch_id = $this->get_gp_branch_id( $slug, $branch );
     576
     577                if ( empty( $branch_id ) )
     578                        return 0;
     579
     580                $translation_set_id = $this->db->get_var( $this->db->prepare(
     581                        'SELECT id FROM translate_translation_sets WHERE project_id = %d AND locale = %s',
     582                        $branch_id, $locale ) );
     583
     584                if ( empty( $translation_set_id ) ) {
     585                        // Don't give up yet. Might be given fr_FR, which actually exists as locale=fr in GP.
     586                        $translation_set_id = $this->db->get_var( $this->db->prepare(
     587                                'SELECT id FROM translate_translation_sets WHERE project_id = %d AND locale = %s',
     588                                $branch_id, substr( $locale, 0, 2 ) ) );
     589                }
     590
     591                if ( empty( $translation_set_id ) )
     592                        $translation_set_id = 0;
     593
     594                $this->cache_set( $slug, $branch, $translation_set_id, $cache_suffix );
     595
     596                return $translation_set_id;
     597        }
     598
     599        function search_gp_original( $slug, $branch, $key, $str ) {
     600                $cache_suffix = "original:{$key}";
     601
     602                if ( false !== ( $original = $this->cache_get( $slug, $branch, $cache_suffix ) ) )
     603                        return $original;
     604
     605                $branch_id = $this->get_gp_branch_id( $slug, $branch );
     606
     607                if ( empty( $branch_id ) )
     608                        return false;
     609
     610                $original = $this->db->get_row( $this->db->prepare(
     611                        'SELECT id, singular, comment FROM translate_originals WHERE project_id = %d AND status = %s AND singular = %s',
     612                        $branch_id, '+active', $str
     613                ) );
     614
     615                if ( empty( $original ) )
     616                        $original = null;
     617
     618                $this->cache_set( $slug, $branch, $original, $cache_suffix );
     619
     620                return $original;
     621        }
     622
     623        function translate_gp_original( $original, $translation, $content) {
     624                $content = str_replace( $original, $translation, $content );
     625                return $content;
     626        }
     627
     628        function verify_subdomain( $locale ) {
     629                if ( empty( $locale ) )
     630                        return '';
     631
     632                $cache_key = "subdomains:{$locale}";
     633
     634                wp_cache_delete( $cache_key, $this->i18n_cache_group );
     635                if ( false !== ( $subdomain = wp_cache_get( $cache_key, $this->i18n_cache_group ) ) ) {
     636                        // var_dump(array($cache_key, $subdomain));
     637                        return $subdomain;
     638                }
     639
     640                $subdomain = '';
     641
     642                if ( 2 === strlen( $locale ) ) {
     643                        $subdomain = $this->db->get_var( $this->db->prepare(
     644                                'SELECT subdomain FROM locales WHERE subdomain = %s LIMIT 1',
     645                                $locale
     646                        ) );
     647                } else if ( 5 === strlen( $locale ) ) {
     648                        $subdomain = $this->db->get_var( $this->db->prepare(
     649                                'SELECT subdomain FROM locales WHERE locale = %s LIMIT 1',
     650                                $locale
     651                        ) );
     652                }
     653
     654                wp_cache_set( $cache_key, $subdomain, $this->i18n_cache_group );
     655
     656                return $subdomain;
     657        }
     658}
     659 No newline at end of file
  • translate/bin/set-wp-plugin-project.php

    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
     
     1<?php
     2/*
     3 * Sets the required GlotPress subproject environment for a WordPress plugin hosted on
     4 * EG: https://translate.wordpress.org/projects/wp-plugins/livejournal-importer/
     5 * * dev, dev-readme[, stable, stable-readme]
     6 * * starts with the same translation sets as existing on https://translate.wordpress.org/projects/wp-plugins/wordpress-importer/dev
     7 */
     8
     9// Test script arguments.
     10
     11if ( $argc < 2 || !preg_match( '/^[^\/]+$/', $argv[1] ) ) {
     12        echo "Usage: $argv[0] plugin-slug\n";
     13        echo "Example:\n$argv[0] akismet\n";
     14        exit( 1 );
     15}
     16
     17/*
     18 * Define environments, paths, supported languages.
     19 */
     20
     21$master_project_path = 'wp-plugins';
     22$plugin_slug         = trim( strtolower( $argv[1] ) );
     23$plugin_project_path = "{$master_project_path}/{$plugin_slug}";
     24
     25/*
     26 * Make sure we want to process that plugin, or exit on failure.
     27 * TODO: remove when we go live for all plugins
     28 */
     29
     30$valid_plugins = array(
     31        'akismet', 'wpcat2tag-importer', 'wordpress-importer',
     32        'utw-importer', 'textpattern-importer', 'stp-importer',
     33        'rss-importer', 'opml-importer', 'movabletype-importer',
     34        'livejournal-importer', 'greymatter-importer', 'dotclear-importer',
     35        'blogware-importer', 'blogger-importer', 'tumblr-importer',
     36        'bbpress', 'wordpress-beta-tester', ''
     37);
     38
     39if ( !in_array( $plugin_slug, $valid_plugins ) ) {
     40        echo "Sorry, but {$plugin_slug} is not a plugin we wish to process at the moment.\n";
     41        exit( 1 );
     42}
     43
     44/*
     45 *  Load GP.
     46 */
     47
     48include __DIR__ . '/../glotpress/gp-load.php';
     49
     50/*
     51 * Get data for master parent project, or exit on failure.
     52 */
     53
     54$master_project = GP::$project->by_path( $master_project_path );
     55
     56if ( empty( $master_project ) ) {
     57        echo "Sorry, but couldn't find https://translate.wordpress.org/projects/{$master_project_path}.\n";
     58        exit( 1 );
     59}
     60
     61/*
     62 * Get the plugin's code, and name from the latter, or exit on failure.
     63 */
     64
     65$plugin_code_content = file_get_contents( "https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/{$plugin_slug}.php" );
     66
     67if ( empty( $plugin_code_content ) ) {
     68        echo "Sorry, but couldn't find https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/{$plugin_slug}.php.\n";
     69        exit( 1 );
     70}
     71
     72$plugin_name = search_string( $plugin_code_content, '/Plugin Name: (.+)/' );
     73
     74/*
     75 * Get or create the plugin GP project, or exit on failure
     76 */
     77
     78$plugin_project = GP::$project->by_path( $plugin_project_path );
     79
     80if ( empty( $plugin_project ) ) {
     81        $plugin_desc = search_string( $plugin_code_content, '/Description: (.+)/' );
     82
     83        $plugin_project = GP::$project->create( new GP_Project( array(
     84                'name'              => $plugin_name,
     85                'slug'              => $plugin_slug,
     86                'parent_project_id' => $master_project->id,
     87                'description'       => $plugin_desc,
     88                'active'            => 1,
     89        ) ) );
     90
     91        if ( empty( $plugin_project ) ) {
     92                echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$plugin_project_path}.\n";
     93                exit( 1 );
     94        }
     95
     96        echo "Created https://translate.wordpress.org/projects/{$plugin_project_path}.\n";
     97}
     98
     99/*
     100 * Define if this plugin will need both a dev and stable branch, or just the former.
     101 * Exit if we can't find a readme file or declared stable tag (supports trunk as the latter).
     102 */
     103
     104$plugin_readme_content = file_get_contents( "https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/readme.txt" );
     105
     106if ( empty( $plugin_readme_content ) ) {
     107        echo "Sorry, but couldn't find https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/readme.txt.\n";
     108        exit( 1 );
     109}
     110
     111$stable_tag = search_string( $plugin_readme_content, '/Stable tag: (.+)/' );
     112
     113if ( empty( $stable_tag ) ) {
     114        echo "Sorry, but couldn't find a stable tag in https://plugins.svn.wordpress.org/{$plugin_slug}/trunk/readme.txt.\n";
     115        exit( 1 );
     116}
     117
     118$needs_stable = ( 'trunk' !== $stable_tag ) ? true : false;
     119
     120/*
     121 * Deal with the always-existing dev and dev-readme branches.
     122 */
     123
     124$plugin_dev_branches_projects = handle_plugin_project_branches( $plugin_project_path, $plugin_name, $plugin_project->id, false );
     125
     126/*
     127 * Deal with the stable and stable-readme branches, if needed.
     128 */
     129
     130if ( $needs_stable ) {
     131        $plugin_stable_branches_projects = handle_plugin_project_branches( $plugin_project_path, $plugin_name, $plugin_project->id, true );
     132}
     133
     134/*
     135 * Deal with initial translation sets.
     136 * Using https://translate.wordpress.org/projects/wp-plugins/wordpress-importer/dev as a template for defaults.
     137 */
     138
     139$wp_importer_project = GP::$project->by_path( "{$master_project_path}/wordpress-importer/dev" );
     140
     141if ( ! empty( $wp_importer_project ) && !empty( $plugin_dev_branches_projects ) ) {
     142        $translation_sets = (array) GP::$translation_set->by_project_id( $wp_importer_project->id );
     143
     144        if ( !empty( $translation_sets ) ) {
     145                add_translation_sets_to_branches( $translation_sets, $plugin_dev_branches_projects );
     146
     147                if ( ! empty( $plugin_stable_branches_projects ) ) {
     148                        add_translation_sets_to_branches( $translation_sets, $plugin_stable_branches_projects );
     149                }
     150        }
     151}
     152
     153/*
     154 * We're done!
     155 */
     156echo "All set with https://translate.wordpress.org/projects/{$plugin_project_path}\n";
     157exit( 0 );
     158
     159
     160
     161
     162function search_string( $text, $pattern ) {
     163        if ( empty( $text ) || empty( $pattern ) )
     164                return '';
     165        preg_match( $pattern, $text, $matches );
     166        if ( empty( $matches ) || empty( $matches[1] ) || is_array( $matches[1] ) )
     167                return '';
     168        // Do clean out potential comment closing delimiter (*/) out of string
     169        return preg_replace( '|^(.+)[\s]+?\*/$|', '\1', trim( $matches[1] ) );
     170}
     171
     172function handle_plugin_project_branches( $plugin_project_path, $plugin_name, $parent_id, $for_stable ) {
     173        if ( true === $for_stable ) {
     174                $branch_slug = 'stable';
     175        } else {
     176                $branch_slug = 'dev';
     177                $for_stable  = false;
     178        }
     179
     180        $code_branch_project_path = "{$plugin_project_path}/{$branch_slug}";
     181        $code_branch_project      = GP::$project->by_path( $code_branch_project_path );
     182
     183        if ( empty( $code_branch_project ) ) {
     184                $code_branch_project = GP::$project->create( new GP_Project( array(
     185                        'name'              => ( $for_stable ) ? 'Stable (latest release)' : 'Development (trunk)',
     186                        'slug'              => $branch_slug,
     187                        'parent_project_id' => $parent_id,
     188                        'description'       => ( ( $for_stable ) ? 'Stable' : 'Development' ) . " version of the {$plugin_name} plugin.",
     189                        'active'            => 1,
     190                ) ) );
     191
     192                if ( empty( $code_branch_project ) ) {
     193                        echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$code_branch_project_path}.\n";
     194                } else {
     195                        echo "Created https://translate.wordpress.org/projects/{$code_branch_project_path}.\n";
     196                }
     197        }
     198
     199        $readme_branch_project_path = "{$plugin_project_path}/{$branch_slug}-readme";
     200        $readme_branch_project      = GP::$project->by_path( $readme_branch_project_path );
     201
     202        if ( empty( $readme_branch_project ) ) {
     203                $readme_branch_project = GP::$project->create( new GP_Project( array(
     204                        'name'              => ( $for_stable ) ? 'Stable Readme (latest release)' : 'Development Readme (trunk)',
     205                        'slug'              => "{$branch_slug}-readme",
     206                        'parent_project_id' => $parent_id,
     207                        'description'       => ( ( $for_stable ) ? 'Stable' : 'Development' ) . " version of the {$plugin_name} plugin's readme.txt file.",
     208                        'active'            => 1,
     209                ) ) );
     210
     211                if ( empty( $readme_branch_project ) ) {
     212                        echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$readme_branch_project_path}.\n";
     213                } else {
     214                        echo "Created https://translate.wordpress.org/projects/{$readme_branch_project_path}.\n";
     215                }
     216        }
     217
     218        return array( 'code' => $code_branch_project, 'readme' => $readme_branch_project );
     219}
     220
     221function add_translation_sets_to_branches( $translation_sets, $branches_projects ) {
     222        foreach( (array) $branches_projects as $project ) {
     223                if ( empty( $project ) || empty( $project->id ) )
     224                        continue;
     225
     226                foreach ( $translation_sets as $ts ) {
     227                        if ( empty( $ts ) || empty( $ts->name ) )
     228                                continue;
     229
     230                        $existing = GP::$translation_set->by_project_id_slug_and_locale( $project->id, $ts->slug, $ts->locale );
     231
     232                        if ( !empty($existing) )
     233                                continue;
     234
     235                        $new_ts = GP::$translation_set->create( array(
     236                                'project_id' => $project->id,
     237                                'name'       => $ts->name,
     238                                'locale'     => $ts->locale,
     239                                'slug'       => $ts->slug,
     240                        ) );
     241
     242                        if ( empty( $new_ts ) ) {
     243                                echo "Sorry, but couldn't create nonexistent https://translate.wordpress.org/projects/{$project->path}/{$ts->locale}/{$ts->slug}.\n";
     244                        } else {
     245                                echo "Created https://translate.wordpress.org/projects/{$project->path}/{$ts->locale}/{$ts->slug}.\n";
     246                        }
     247                }
     248        }
     249}
     250 No newline at end of file
  • extend/plugins-plugins/svn-track/class.dotorg-plugins-tracker.php

    Property changes on: translate/bin/set-wp-plugin-project.php
    ___________________________________________________________________
    Added: svn:eol-style
    ## -0,0 +1 ##
    +native
    \ No newline at end of property
     
    8484                add_action( 'bb_new_post',  array(&$this, 'bb_new_post'),  -1 );
    8585                add_action( 'bb_update_post',  array(&$this, 'bb_new_post'),  -1 );
    8686
     87                add_filter( 'topic_title', array( &$this, 'translate_title' ) );
     88                add_filter( 'topic_link',  array( &$this, 'translate_link' ) );
     89                add_filter( 'search_link', array( &$this, 'translate_link' ) );
     90
    8791                bb_register_view( 'new',     __( 'Newest', 'wporg' ),           array( 'order_by' => 'topic_start_time', 'sticky' => 'all', 'open' => 1 ) );
    8892                bb_register_view( 'updated', __( 'Recently Updated', 'wporg' ), array( 'order_by' => 'topic_time', 'sticky' => 'all', 'open' => 1 ) );
    8993                bb_register_view( 'popular', __( 'Most Popular', 'wporg' ),     array( 'meta_key' => 'downloads', 'order_by' => '0 + tm.meta_value', 'sticky' => 'all', 'open' => 1 ) );
     
    9397                                bb_set_current_user( $user->ID );
    9498                        }
    9599
    96                         if ( 1 < $argc && 'update' == $argv[1] ) {
     100                        if ( 1 < $argc && ( 'update' == $argv[1] || 'i18n' == $argv[1] ) ) {
    97101                                if ( !isset( $_SERVER['REMOTE_ADDR'] ) )
    98102                                        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
    99103
    100104                                if ( 2 == $argc ) // php bb-load.php update
    101105                                        return $this->process_changes();
    102106
     107                                if ( 'i18n' == $argv[1] ) {
     108                                        // php plugins/bb-load.php i18n slug [dev|stable, default: dev] [code|readme|all, default: all]
     109                                        return $this->process_i18n( $argv[2], ( 'stable' === $argv[3] ) ? $argv[3] : 'dev', $argv[4] );
     110                                }
     111
    103112                                switch ( $argv[2] ) {
    104113                                case 'all' :
    105114                                        return $this->process_all();
     
    277286                        $content = preg_replace_callback( "#<img class='screenshot' src='(screenshot-[^']+)' alt='[^']+' />#", array( &$this, '_screenshot_img_tag' ), $content );
    278287                }
    279288
    280                 return $content;
     289                return $this->translate( $show, $content );
    281290        }
    282291
     292        function i18n_obj( $db, $tracker = null ) {
     293                if ( !class_exists( 'Dotorg_Plugin_I18n' ) )
     294                        require_once( __DIR__ . '/class.dotorg-plugins-i18n.php' );
     295                return new Dotorg_Plugin_I18n( $db, $tracker );
     296        }
     297
     298        function translate( $key, $content, $args = array() ) {
     299                return $this->i18n_obj( $this->db )->translate( $key, $content, $args );
     300        }
     301
     302        /*
     303         * Used by:
     304         * * add_filter( 'topic_title', array( &$this, 'translate_title' ) );
     305         */
     306        function translate_title( $string ) {
     307                return $this->translate( 'title', $string );
     308        }
     309
     310        /*
     311         * Converts a WP Plugins Directory link into the related localized link,
     312         * when on localized hosts (eg: fr.wordpress.org).
     313         *
     314         * Used by:
     315         * * add_filter( 'topic_link',  array( &$this, 'translate_link' ) );
     316         * * add_filter( 'search_link', array( &$this, 'translate_link' ) );
     317         * * self::session_url()
     318         */
     319        function translate_link( $string ) {
     320                $server_name = strtolower( $_SERVER['SERVER_NAME'] );
     321                if ( false === strpos( $string, 'wordpress.org/plugins' )                      // passed string is uninteresting
     322                        || preg_match( '|^(https?:)?//[^\.]{2}\.wordpress\.org(\/.*)?|', $string ) // passed string is already a localized wp.org host
     323                        || !preg_match( '|^[^\.]{2}\.wordpress\.org$|', $server_name ) )           // server name not a localized wp.org host
     324                                return $string;
     325                return str_replace( '//wordpress.org/plugins/', '//' . $server_name . '/plugins/', $string );
     326        }
     327
    283328        function _screenshot_shortcode( $matches ) {
    284329                return $this->_screenshot_image( $matches[1], $matches[2] );
    285330        }
     
    13081353                                $path_rels = array_merge( $path_rels, array_keys( $entry->paths ) );
    13091354                        }
    13101355                        $return_revision = array_shift( array_keys( $triggers ) );
    1311                 } else { // its some paths
     1356                } else { // its some paths
    13121357                        $path_rels =& $triggers;
    13131358                        $return_revision = true;
    13141359                }
    13151360
     1361                // Reprocess code and/or readme i18n (method figures out if actually needed)
     1362                // $this->i18n_obj( $this->db, $this )->process_paths_array( $path_rels );
     1363
    13161364                if ( ( isset($types['roots']) && $types['roots'] ) || ( isset($types['paths_in_roots']) && $types['paths_in_roots'] ) ) {
    13171365                        if ( 'grouped' === $types['roots'] ) // roots are pre-grouped
    13181366                                $roots =& $path_rels;
     
    17021750                        else
    17031751                                bb_delete_topicmeta( $topic_id, $key );
    17041752                }
     1753
     1754                // Reprocess the readme for translation.
     1755                // DEBUG: placeholder, untested and not ready as of yet.
     1756                // $this->process_i18n( $slug, 'stable', 'readme' ); // Figures on its own if stable means dev (trunk).
    17051757        }
    17061758
    17071759        function load_topic_page( $topic_id ) {
     
    18711923                        return 'http://wordpress.org/support/view/plugin-reviews/' . $topic->topic_slug;
    18721924                if ( bb_get_option( 'mod_rewrite' ) ) {
    18731925                        if ( 'description' == $section )
    1874                                 return rtrim( get_topic_link( $topic_id ), '/' ) . '/';
    1875                         return rtrim( get_topic_link( $topic_id ), '/' ) . "/$section/";
     1926                                return $this->translate_link( rtrim( get_topic_link( $topic_id ), '/' ) . '/' );
     1927                        return  $this->translate_link( rtrim( get_topic_link( $topic_id ), '/' ) . "/$section/" );
    18761928                } else {
    18771929                        if ( 'description' == $section )
    1878                                 return remove_query_arg( 'show', get_topic_link( $topic_id ) );
    1879                         return add_query_arg( 'show', $section, get_topic_link( $topic_id ) );
     1930                                return  $this->translate_link( remove_query_arg( 'show', get_topic_link( $topic_id ) ) );
     1931                        return  $this->translate_link( add_query_arg( 'show', $section, get_topic_link( $topic_id ) ) );
    18801932                }
    18811933        }
    18821934
     
    21422194                return false;
    21432195        }
    21442196
     2197        function process_i18n( $slug, $branch = 'dev', $type = 'all' ) {
     2198                return $this->i18n_obj( $this->db, $this )->process( $slug, $branch, $type );
     2199        }
     2200
    21452201        function get_all_roots( $via = 'local' ) {
    21462202                global $bbdb;
    21472203                $root_rels = false;
  • bb-theme/plugins/search-form.php

     
    1 <form action="<?php bb_uri( 'search.php', null, BB_URI_CONTEXT_FORM_ACTION ); ?>" method="get" id="plugins-search">
     1<form action="<?php echo apply_filters( 'search_link', bb_get_uri( 'search.php', null, BB_URI_CONTEXT_FORM_ACTION ) ); ?>" method="get" id="plugins-search">
    22<p>
    33        <input type="text" class="text" maxlength="100" name="q" value="<?php echo attribute_escape( $q ); ?>" />
    44        <input type="submit" value="<?php echo attribute_escape( __( 'Search Plugins', 'wporg' ) ); ?>" class="button" />
  • bb-theme/plugins/sub.php

     
    4242<?php if ( ! is_front() && ! is_bb_search() ) : ?>
    4343        <h4><?php _e( 'Search Plugins', 'wporg' ); ?></h4>
    4444
    45         <form id="side-search" method="get" action="//wordpress.org/plugins/search.php">
     45        <form id="side-search" method="get" action="<?php echo apply_filters( 'search_link', '//wordpress.org/plugins/search.php' ); ?>">
    4646                <div>
    4747                        <input type="text" class="text" name="q" value="<?php echo esc_attr( empty( $_REQUEST['q'] ) ? "" : attribute_escape( $_REQUEST['q'] ) ); ?>" />
    4848                        <input type="submit" class="button" value="Search" /><br class="clear" />
  • bb-theme/themes/search-form.php

     
    1 <form action="<?php bb_uri('search.php', null, BB_URI_CONTEXT_FORM_ACTION); ?>" method="get" id="plugins-search">
     1<form action="<?php echo apply_filters( 'search_link', bb_get_uri( 'search.php', null, BB_URI_CONTEXT_FORM_ACTION ) ); ?>" method="get" id="plugins-search">
    22<p>
    33        <input type="text" class="text" maxlength="100" name="q" value="<?php echo attribute_escape( $q ); ?>" />
    44        <input type="submit" value="<?php echo attribute_escape( __( 'Search Themes', 'wporg' ) ); ?>" class="button" /></p>
  • bb-theme/wporg/plugin-reviews.php

     
    5959                                <li><a href='/extend/kvetch/'>Kvetch!</a></li>
    6060                        </ul>
    6161
    62 <h4>Search Plugins</h4><form id='side-search' method='get' action='//wordpress.org/plugins/search.php'>
     62<h4>Search Plugins</h4><form id='side-search' method='get' action='<?php echo apply_filters( 'topic_link', '//wordpress.org/plugins/search.php' ); ?>'>
    6363<div>
    6464        <input type='text' class='text' name='q' value='' />
    6565        <input type='submit' class='button' value='Search' /><br class='clear' />
  • bb-theme/wporg/plugin.php

     
    5252                                <li><a href='/extend/kvetch/'>Kvetch!</a></li>
    5353                        </ul>
    5454
    55 <h4>Search Plugins</h4><form id='side-search' method='get' action='//wordpress.org/plugins/search.php'>
     55<h4>Search Plugins</h4><form id='side-search' method='get' action='<?php echo apply_filters( 'topic_link', '//wordpress.org/plugins/search.php' ); ?>'>
    5656<div>
    5757        <input type='text' class='text' name='q' value='' />
    5858        <input type='submit' class='button' value='Search' /><br class='clear' />