Making WordPress.org

Changeset 11122


Ignore:
Timestamp:
07/19/2021 03:13:43 AM (5 years ago)
Author:
dd32
Message:

Translate: Add a CLI command to export .json translation files for a project (plugin/theme) into a WPLANGDIR export folder.

This is to be used by the WordPress.org translation deployment process for the WordPress.org-site primarily, but might be useful in other ways.

See https://github.com/WordPress/pattern-directory/issues/282.

Location:
sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/inc
Files:
1 edited
1 copied

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/inc/class-plugin.php

    r10587 r11122  
    429429                WP_CLI::add_command( 'wporg-translate make-core-pot', __NAMESPACE__ . '\CLI\Make_Core_Pot' );
    430430                WP_CLI::add_command( 'wporg-translate export', __NAMESPACE__ . '\CLI\Export' );
     431                WP_CLI::add_command( 'wporg-translate export-json', __NAMESPACE__ . '\CLI\Export_Json' );
     432
    431433        }
    432434
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/wporg-gp-customizations/inc/cli/class-export-json.php

    r11116 r11122  
    55use GP;
    66use GP_Locales;
    7 use stdClass;
    87use WP_CLI;
    9 use WP_CLI\Utils;
    108use WP_CLI_Command;
    11 use WP_Error;
    12 
    13 class Language_Pack extends WP_CLI_Command {
    14 
    15         const TMP_DIR = '/tmp/language-packs';
    16         const BUILD_DIR = '/nfs/rosetta/builds';
    17         const SVN_URL = 'https://i18n.svn.wordpress.org';
    18         const PACKAGE_THRESHOLD = 90;
    19 
    20         /**
    21          * Whether a language pack should be enforced.
    22          *
    23          * @var bool
    24          */
    25         private $force = false;
    26 
    27         /**
    28          * Generates a language pack.
     9
     10class Export_JSON extends WP_CLI_Command {
     11
     12        /**
     13         * Export plugin/themes JSON translation files for a project into a WPLANGDIR type directory.
    2914         *
    3015         * ## OPTIONS
    3116         *
     17         * <project>
     18         * : The GlotPress project to export.
     19         *
     20         * <locale>
     21         * : Locale to export json translations for.
     22         *
    3223         * <type>
    33          * : Type of the language pack. 'plugin' or 'theme'.
    34          *
    35          * <slug>
    36          * : Slug of the theme or plugin.
    37          *
    38          * [--locale]
    39          * : Locale the language pack is for.
    40          *
     24         * : The type of the project. 'plugin', 'theme', or 'both'.
     25         * 
     26         * <textdomain>
     27         * : The textdomain of the project.
     28         * 
     29         * <export-directory>
     30         * : The directory to export the JSON files into. Must contain `plugins` and `themes` sub-folders.
     31         * 
    4132         * [--locale-slug]
    4233         * : Slug of the locale. Default: 'default'.
    43          *
    44          * [--version]
    45          * : Current version of the theme or plugin.
    46          *
    47          * [--force]
    48          * : Generate language pack even when threshold is not reached or no updates exist.
    49          */
    50         public function generate( $args, $assoc_args ) {
    51                 $type = $args[0];
    52                 $slug = $args[1];
    53 
    54                 $this->force = Utils\get_flag_value( $assoc_args, 'force' );
     34         *
     35         * [--strip-path-prefix]
     36         * : Path prefix to strip from the file prefix. Pass 'guess' to strip all plugin/theme looking prefixes. Default: false.
     37         */
     38        public function __invoke( $args, $assoc_args ) {
     39                $project_slug = $args[0];
     40                $locale       = $args[1];
     41                $type         = $args[2] === 'both' ? false : $args[2];
     42                $textdomain   = $args[3];
     43                $export_dir   = $args[4];
    5544
    5645                $args = wp_parse_args( $assoc_args, [
    57                         'locale'      => false,
    58                         'locale-slug' => 'default',
    59                         'version'     => false,
     46                        'locale-slug'       => 'default',
     47                        'strip-path-prefix' => false,
    6048                ] );
    6149
    62                 switch ( $type ) {
    63                         case 'plugin' :
    64                                 $this->generate_plugin( $slug, $args );
    65                                 break;
    66                         case 'theme' :
    67                                 $this->generate_theme( $slug, $args );
    68                                 break;
    69                         default :
    70                                 WP_CLI::error( 'Invalid type.' );
    71                 }
    72         }
    73 
    74         /**
    75          * Generates a language pack for a plugin.
    76          *
    77          * Examples:
    78          *   wp @translate wporg-translate language-pack generate plugin nothing-much
    79          *   wp @translate wporg-translate language-pack generate plugin nothing-much --locale=de
    80          *
    81          * @param string $slug Slug of the plugin.
    82          * @param array  $args Extra arguments.
    83          */
    84         private function generate_plugin( $slug, $args ) {
    85                 $gp_project = GP::$project->by_path( "wp-plugins/$slug" );
     50                $gp_project = GP::$project->by_path( $project_slug );
    8651                if ( ! $gp_project ) {
    87                         WP_CLI::error( 'Invalid plugin slug.' );
    88                 }
    89 
    90                 $stable_tag = $this->get_plugin_stable_tag( $slug );
    91                 $branch = ( 'trunk' !== $stable_tag ) ? 'stable' : 'dev';
    92 
    93                 $gp_project = GP::$project->by_path( "wp-plugins/$slug/$branch" );
    94                 if ( ! $gp_project ) {
    95                         WP_CLI::error( 'Invalid plugin branch.' );
    96                 }
    97 
    98                 $translation_sets = GP::$translation_set->by_project_id( $gp_project->id );
    99                 if ( ! $translation_sets ) {
    100                         WP_CLI::error( 'No translation sets available.' );
    101                 }
    102 
    103                 /**
    104                  * Filters the arguments passed to the WP-CLI command.
    105                  *
    106                  * @param array  $args CLI arguments.
    107                  * @param string $slug Slug of the theme.
    108                  */
    109                 $args = apply_filters( 'wporg_translate_language_pack_plugin_args', $args, $slug );
    110 
    111                 if ( $args['locale'] ) {
    112                         $translation_sets = wp_list_filter( $translation_sets, [
    113                                 'locale' => $args['locale'],
    114                                 'slug'   => $args['locale-slug'],
    115                         ] );
    116                 }
    117 
    118                 if ( ! $translation_sets ) {
    119                         WP_CLI::error( 'No translation sets available.' );
    120                 }
    121 
    122                 $version = $args['version'];
    123                 if ( ! $version ) {
    124                         $version = $this->get_latest_plugin_version( $slug );
    125                 }
    126 
    127                 if ( ! $version ) {
    128                         WP_CLI::error( 'No version available.' );
    129                 }
    130 
    131                 $svn_command  = $this->get_svn_command();
    132                 $svn_checkout = self::get_temp_directory( $slug );
    133 
    134                 $result = $this->execute_command( sprintf(
    135                         '%s checkout --quiet --depth=empty %s %s 2>&1',
    136                         $svn_command,
    137                         escapeshellarg( self::SVN_URL . '/plugins' ),
    138                         escapeshellarg( $svn_checkout )
    139                 ) );
    140 
    141                 if ( is_wp_error( $result ) ) {
    142                         WP_CLI::error_multi_line( $result->get_error_data() );
    143                         WP_CLI::error( 'SVN export failed.' );
    144                 }
    145 
    146                 $data                   = new stdClass();
    147                 $data->type             = 'plugin';
    148                 $data->domain           = $slug;
    149                 $data->version          = $version;
    150                 $data->translation_sets = $translation_sets;
    151                 $data->gp_project       = $gp_project;
    152                 $data->svn_checkout     = $svn_checkout;
    153                 $this->build_language_packs( $data );
    154         }
    155 
    156         /**
    157          * Generates a language pack for a theme.
    158          *
    159          * Examples:
    160          *   wp @translate wporg-translate language-pack generate theme twentyeleven
    161          *   wp @translate wporg-translate language-pack generate theme twentyeleven --locale=de
    162          *
    163          * @param string $slug Slug of the theme.
    164          * @param array  $args Extra arguments.
    165          */
    166         private function generate_theme( $slug, $args ) {
    167                 $gp_project = GP::$project->by_path( "wp-themes/$slug" );
    168                 if ( ! $gp_project ) {
    169                         WP_CLI::error( 'Invalid theme slug.' );
    170                 }
    171 
    172                 $translation_sets = GP::$translation_set->by_project_id( $gp_project->id );
    173                 if ( ! $translation_sets ) {
    174                         WP_CLI::error( 'No translation sets available.' );
    175                 }
    176 
    177                 /**
    178                  * Filters the arguments passed to the WP-CLI command.
    179                  *
    180                  * @param array  $args CLI arguments.
    181                  * @param string $slug Slug of the theme.
    182                  */
    183                 $args = apply_filters( 'wporg_translate_language_pack_theme_args', $args, $slug );
    184 
    185                 if ( $args['locale'] ) {
    186                         $translation_sets = wp_list_filter( $translation_sets, [
    187                                 'locale' => $args['locale'],
    188                                 'slug'   => $args['locale-slug'],
    189                         ] );
    190                 }
    191 
    192                 if ( ! $translation_sets ) {
    193                         WP_CLI::error( 'No translation sets available.' );
    194                 }
    195 
    196                 $version = $args['version'];
    197                 if ( ! $version ) {
    198                         $version = $this->get_latest_theme_version( $slug );
    199                 }
    200 
    201                 if ( ! $version ) {
    202                         WP_CLI::error( 'No version available.' );
    203                 }
    204 
    205                 $svn_command  = $this->get_svn_command();
    206                 $svn_checkout = self::get_temp_directory( $slug );
    207 
    208                 $result = $this->execute_command( sprintf(
    209                         '%s checkout --quiet --depth=empty %s %s 2>&1',
    210                         $svn_command,
    211                         escapeshellarg( self::SVN_URL . '/themes' ),
    212                         escapeshellarg( $svn_checkout )
    213                 ) );
    214 
    215                 if ( is_wp_error( $result ) ) {
    216                         WP_CLI::error_multi_line( $result->get_error_data() );
    217                         WP_CLI::error( 'SVN export failed.' );
    218                 }
    219 
    220                 $data                   = new stdClass();
    221                 $data->type             = 'theme';
    222                 $data->domain           = $slug;
    223                 $data->version          = $version;
    224                 $data->translation_sets = $translation_sets;
    225                 $data->gp_project       = $gp_project;
    226                 $data->svn_checkout     = $svn_checkout;
    227                 $this->build_language_packs( $data );
    228         }
    229 
    230         /**
    231          * Creates a unique temporary directory.
    232          *
    233          * The temporary directory returned will be removed upon script termination.
    234          *
    235          * @param string $prefix Optional. The prefix for the directory, 'hello-dolly' for example.
    236          * @return string The temporary directory.
    237          */
    238         public static function get_temp_directory( $prefix = '' ) {
    239                 if ( ! is_dir( self::TMP_DIR ) ) {
    240                         mkdir( self::TMP_DIR, 0777 );
    241                 }
    242 
    243                 // Generate a unique filename
    244                 $tmp_dir = tempnam( self::TMP_DIR, $prefix );
    245 
    246                 // Replace that filename with a directory
    247                 unlink( $tmp_dir );
    248                 mkdir( $tmp_dir, 0777 );
    249 
    250                 // Automatically remove temporary directories on shutdown.
    251                 register_shutdown_function( [ __CLASS__, 'remove_temp_directory' ], $tmp_dir );
    252 
    253                 return $tmp_dir;
    254         }
    255 
    256         /**
    257          * Removes a directory.
    258          *
    259          * @param string $dir The directory which should be removed.
    260          * @return bool False if directory is removed, false otherwise.
    261          */
    262         public static function remove_temp_directory( $dir ) {
    263                 if ( trim( $dir, '/' ) ) {
    264                         exec( 'rm -rf ' . escapeshellarg( $dir ) );
    265                 }
    266 
    267                 return is_dir( $dir );
    268         }
    269 
    270         /**
    271          * Retrieves the stable tag of a plugin.
    272          *
    273          * @param string $plugin_slug Slug of a plugin.
    274          * @return false|string False on failure, stable tag on success.
    275          */
    276         private function get_plugin_stable_tag( $plugin_slug ) {
    277                 $plugin = @file_get_contents( "https://api.wordpress.org/plugins/info/1.0/{$plugin_slug}.json?fields=stable_tag" );
    278                 if ( ! $plugin ) {
    279                         return false;
    280                 }
    281 
    282                 $plugin = json_decode( $plugin );
    283 
    284                 return $plugin->stable_tag;
    285         }
    286 
    287         /**
    288          * Retrieves the current stable version of a theme.
    289          *
    290          * @param string $theme_slug Slug of a theme.
    291          * @return false|string False on failure, version on success.
    292          */
    293         private function get_latest_theme_version( $theme_slug ) {
    294                 $theme = @file_get_contents( "https://api.wordpress.org/themes/info/1.1/?action=theme_information&request[slug]={$theme_slug}" );
    295                 if ( ! $theme ) {
    296                         return false;
    297                 }
    298 
    299                 $theme = json_decode( $theme );
    300 
    301                 return $theme->version;
    302         }
    303 
    304         /**
    305          * Retrieves the current stable version of a plugin.
    306          *
    307          * @param string $plugin_slug Slug of a plugin.
    308          * @return false|string False on failure, version on success.
    309          */
    310         private function get_latest_plugin_version( $plugin_slug ) {
    311                 $plugin = @file_get_contents( "https://api.wordpress.org/plugins/info/1.0/{$plugin_slug}.json" );
    312                 if ( ! $plugin ) {
    313                         return false;
    314                 }
    315 
    316                 $plugin = json_decode( $plugin );
    317 
    318                 return $plugin->version;
    319         }
    320 
    321         /**
    322          * Retrieves active language packs for a theme/plugin of a version.
    323          *
    324          * @param string $type    Type of the language pack. Supports 'plugin' or 'theme'.
    325          * @param string $domain  Slug of a theme/plugin.
    326          * @param string $version Version of a theme/plugin.
    327          * @return array Array of active language packs.
    328          */
    329         private function get_active_language_packs( $type, $domain, $version ) {
    330                 global $wpdb;
    331 
    332                 $active_language_packs = $wpdb->get_results( $wpdb->prepare(
    333                         'SELECT language, updated FROM language_packs WHERE type = %s AND domain = %s AND version = %s AND active = 1',
    334                         $type,
    335                         $domain,
    336                         $version
    337                 ), OBJECT_K );
    338 
    339                 if ( ! $active_language_packs ) {
    340                         return [];
    341                 }
    342 
    343                 return $active_language_packs;
    344         }
    345 
    346         /**
    347          * Whether there's at least one active language pack for a theme/plugin of any version.
    348          *
    349          * @param string $type   Type of the language pack. Supports 'plugin' or 'theme'.
    350          * @param string $domain Slug of a theme/plugin.
    351          * @param string $locale WordPress locale.
    352          * @return boolean True if theme/plugin has an active language pack, false if not.
    353          */
    354         private function has_active_language_pack( $type, $domain, $locale ) {
    355                 global $wpdb;
    356 
    357                 return (bool) $wpdb->get_var( $wpdb->prepare(
    358                         'SELECT updated FROM language_packs WHERE type = %s AND domain = %s AND language = %s AND active = 1 LIMIT 1',
    359                         $type,
    360                         $domain,
    361                         $locale
    362                 ) );
    363         }
    364 
    365         /**
    366          * Retrieves the basic SVN command with authentication data.
    367          *
    368          * @return string The SVN command.
    369          */
    370         private function get_svn_command() {
    371                 return sprintf(
    372                         'svn --no-auth-cache --non-interactive --username=%s --password=%s',
    373                         escapeshellarg( POT_SVN_USER ),
    374                         escapeshellarg( POT_SVN_PASS )
    375                 );
    376         }
    377 
    378         /**
    379          * Updates a SVN directory.
    380          *
    381          * Creates directories if they don't exist yet.
    382          *
    383          * @param string $svn_directory The SVN directory.
    384          */
    385         private function update_svn_directory( $svn_directory ) {
    386                 $svn_command = $this->get_svn_command();
    387 
    388                 // Update existing files.
    389                 $this->execute_command( sprintf(
    390                         '%s up --quiet --parents %s 2>/dev/null',
    391                         $svn_command,
    392                         escapeshellarg( $svn_directory )
    393                 ) );
    394 
    395                 // Create missing SVN directories.
    396                 $this->execute_command( sprintf(
    397                         '%s mkdir --quiet --parents %s 2>/dev/null',
    398                         $svn_command,
    399                         escapeshellarg( $svn_directory )
    400                 ) );
     52                        WP_CLI::error( 'Invalid project slug.' );
     53                }
     54
     55                if ( ! is_writable( $export_dir ) || ! is_dir( $export_dir ) ) {
     56                        WP_CLI::error( 'Invalid export directory.' );
     57                }
     58
     59                $translation_set = GP::$translation_set->by_project_id_slug_and_locale( $gp_project->id, $args['locale-slug'], $locale );
     60                if ( ! $translation_set ) {
     61                        WP_CLI::error( 'Invalid translation set.' );
     62                }
     63
     64                // Get WP locale.
     65                $gp_locale = GP_Locales::by_slug( $translation_set->locale );
     66                if ( ! isset( $gp_locale->wp_locale ) ) {
     67                        WP_CLI::error( 'Invalid translation set.' );
     68                }
     69
     70                // Check if any current translations exist.
     71                if ( 0 === $translation_set->current_count() ) {
     72                        WP_CLI::log( 'No current translations available.' );
     73                        return;
     74                }
     75
     76                $entries = GP::$translation->for_export( $gp_project, $translation_set, [ 'status' => 'current' ] );
     77                if ( ! $entries ) {
     78                        WP_CLI::warning( 'No current translations available.' );
     79                        return;
     80                }
     81
     82                // Build a mapping based on where the translation entries occur.
     83                $plugins = $this->build_mapping( $entries, 'plugins', $type );
     84                $themes  = $this->build_mapping( $entries, 'themes', $type );
     85
     86                // Strip the path prefix if required.
     87                if ( $args['strip-path-prefix'] ) {
     88                        $plugins = $this->strip_path_prefix( $plugins, $args['strip-path-prefix'] );
     89                        $themes  = $this->strip_path_prefix( $themes, $args['strip-path-prefix'] );
     90                }
     91
     92                // Create JED json files for each JS file.
     93                $plugin_json_files = $this->build_json_files( $gp_project, $gp_locale, $set, $plugins, "{$export_dir}/plugins/{$textdomain}-{$gp_locale->wp_locale}" );
     94                $theme_json_files  = $this->build_json_files( $gp_project, $gp_locale, $set, $themes, "{$export_dir}/themes/{$textdomain}-{$gp_locale->wp_locale}" );
     95
     96                WP_CLI::success( "JSON Files for {$project_slug} {$locale} generated." );
    40197        }
    40298
    40399        /**
    404100         * Build a mapping of JS files to translation entries occurring in those files.
    405          * Translation entries occurring in other files are added to the 'po' key.
    406          *
    407          * @param Translation_Entry[] $entries The translation entries to map.
     101         * Translation entries occurring in other files are skipped.
     102         *
     103         * @param Translation_Entry[] $entries      The translation entries to map.
     104         * @param string              $type_limiter Limit to this kind of string. 'themes', 'plugins'.
     105         * @param string              $type         The type of the project. false to guess.
    408106         * @return array The mapping of sources to translation entries.
    409107         */
    410         private function build_mapping( $entries ) {
     108        private function build_mapping( $entries, $type_limiter, $type ) {
    411109                $mapping = [];
    412110
     
    428126                                                        return $file;
    429127                                                }
    430                                                 return 'po';
     128
     129                                                return false;
    431130                                        },
    432131                                        $entry->references
     
    434133
    435134                                $sources = array_unique( $sources );
     135                        }
     136
     137                        foreach ( array_filter( $sources ) as $source ) {
     138                                if ( $type_limiter ) {
     139                                        $filetype = $type;
     140                                        if ( $type ) {
     141                                                $filetype = $type;
     142                                        } elseif ( preg_match( '!wp-content/(themes|plugins)/!', $source, $m ) ) {
     143                                                $filetype = $m[1];
     144                                        } elseif ( preg_match( '!^/?(themes|plugins)/!', $source, $m ) ) {
     145                                                $filetype = $m[1];
     146                                        }
     147
     148                                        if ( $filetype !== $type_limiter ) {
     149                                                continue;
     150                                        }
     151                                }
     152
     153                                $mapping[ $source ][] = $entry;
     154                        }
     155                }
     156
     157                return $mapping;
     158        }
     159
     160        /**
     161         * Strip a leading path off the keys of the mapping array.
     162         *
     163         * @param array  $mapping The mapping array returned by build_mapping.
     164         * @param string $prefix  The prefix to strip, or 'magic' to strip all plugin/theme-looking paths.
     165         * @return array The $mapping object with paths striped.
     166         */
     167        private function strip_path_prefix( $mapping, $prefix ) {
     168                $result = [];
     169
     170                foreach ( $mapping as $file => $entries ) {
     171                        if ( 'guess' === $prefix ) {
     172                                $file = preg_replace( '!^(.*wp-content)?/?(plugins|themes)/[^/]+/!i', '', $file );
    436173                        } else {
    437                                 $sources = [ 'po' ];
    438                         }
    439 
    440                         foreach ( $sources as $source ) {
    441                                 $mapping[ $source ][] = $entry;
    442                         }
    443                 }
    444 
    445                 return $mapping;
     174                                if ( $prefix === substr( $file, 0, strlen( $prefix ) ) ) {
     175                                        $file = substr( $file, strlen( $prefix ) );
     176                                }
     177                        }
     178
     179                        $result[ $file ] = $entries;
     180                }
     181
     182                return $result;
    446183        }
    447184
     
    486223        }
    487224
    488         /**
    489          * Builds a PO file for translations.
    490          *
    491          * @param GP_Project          $gp_project The GlotPress project.
    492          * @param GP_Locale           $gp_locale  The GlotPress locale.
    493          * @param GP_Translation_Set  $set        The translation set.
    494          * @param Translation_Entry[] $entries    The translation entries.
    495          * @param string              $dest       Destination file name.
    496          * @return string|WP_Error Last updated date on success, WP_Error on failure.
    497          */
    498         private function build_po_file( $gp_project, $gp_locale, $set, $entries, $dest ) {
    499                 $format     = gp_array_get( GP::$formats, 'po' );
    500                 $po_content = $format->print_exported_file( $gp_project, $gp_locale, $set, $entries );
    501 
    502                 // Get last updated.
    503                 preg_match( '/^"PO-Revision-Date: (.*)\+\d+\\\n/m', $po_content, $match );
    504                 if ( empty( $match[1] ) ) {
    505                         return new WP_Error( 'invalid_format', 'Date could not be parsed.' );
    506                 }
    507 
    508                 file_put_contents( $dest, $po_content );
    509 
    510                 return $match[1];
    511         }
    512 
    513         /**
    514          * Executes a command via exec().
    515          *
    516          * @param string $command The escaped command to execute.
    517          * @return true|WP_Error True on success, WP_Error on failure.
    518          */
    519         private function execute_command( $command ) {
    520                 exec( $command, $output, $return_var );
    521 
    522                 if ( $return_var ) {
    523                         return new WP_Error( $return_var, 'Error while executing the command.', $output );
    524                 }
    525 
    526                 return true;
    527         }
    528 
    529         /**
    530          * Inserts a language pack into database.
    531          *
    532          * @param string $type     Type of the language pack.
    533          * @param string $domain   Slug of the theme/plugin.
    534          * @param string $language Language the language pack is for.
    535          * @param string $version  Version of the theme/plugin.
    536          * @param string $updated  Last updated.
    537          * @return true|WP_Error true when language pack was updated, WP_Error on failure.
    538          */
    539         private function insert_language_pack( $type, $domain, $language, $version, $updated ) {
    540                 global $wpdb;
    541 
    542                 $existing = $wpdb->get_var( $wpdb->prepare(
    543                         'SELECT id FROM language_packs WHERE type = %s AND domain = %s AND language = %s AND version = %s AND updated = %s AND active = 1',
    544                         $type,
    545                         $domain,
    546                         $language,
    547                         $version,
    548                         $updated
    549                 ) );
    550 
    551                 if ( $existing ) {
    552                         return true;
    553                 }
    554 
    555                 $now = current_time( 'mysql', 1 );
    556                 $inserted = $wpdb->insert( 'language_packs', [
    557                         'type'          => $type,
    558                         'domain'        => $domain,
    559                         'language'      => $language,
    560                         'version'       => $version,
    561                         'updated'       => $updated,
    562                         'active'        => 1,
    563                         'date_added'    => $now,
    564                         'date_modified' => $now,
    565                 ] );
    566 
    567                 if ( ! $inserted ) {
    568                         return new WP_Error( 'language_pack_not_inserted', 'The language pack was not inserted.' );
    569                 }
    570 
    571                 // Mark old language packs for the same version as inactive.
    572                 $wpdb->query( $wpdb->prepare(
    573                         'UPDATE language_packs SET active = 0, date_modified = %s WHERE type = %s AND domain = %s AND language = %s AND version = %s AND id <> %d',
    574                         $now,
    575                         $type,
    576                         $domain,
    577                         $language,
    578                         $version,
    579                         $wpdb->insert_id
    580                 ) );
    581 
    582                 return true;
    583         }
    584 
    585         /**
    586          * Builds a language pack.
    587          *
    588          * @param object $data The data of a language pack.
    589          */
    590         private function build_language_packs( $data ) {
    591                 $existing_packs = $this->get_active_language_packs( $data->type, $data->domain, $data->version );
    592                 $svn_command    = $this->get_svn_command();
    593 
    594                 foreach ( $data->translation_sets as $set ) {
    595                         // Get WP locale.
    596                         $gp_locale = GP_Locales::by_slug( $set->locale );
    597                         if ( ! isset( $gp_locale->wp_locale ) ) {
    598                                 continue;
    599                         }
    600 
    601                         // Change wp_locale until GlotPress returns the correct wp_locale for variants.
    602                         $wp_locale = $gp_locale->wp_locale;
    603                         if ( 'default' !== $set->slug ) {
    604                                 $wp_locale = $wp_locale . '_' . $set->slug;
    605                         }
    606 
    607                         // Check if any current translations exist.
    608                         if ( 0 === $set->current_count() ) {
    609                                 WP_CLI::log( "Skip {$wp_locale}, no translations." );
    610                                 continue;
    611                         }
    612 
    613                         // Check if percent translated is above threshold for initial language pack.
    614                         $has_existing_pack = $this->has_active_language_pack( $data->type, $data->domain, $wp_locale );
    615                         if ( ! $has_existing_pack ) {
    616                                 $percent_translated = $set->percent_translated();
    617                                 if ( ! $this->force && $percent_translated < self::PACKAGE_THRESHOLD ) {
    618                                         WP_CLI::log( "Skip {$wp_locale}, translations below threshold ({$percent_translated}%)." );
    619                                         continue;
    620                                 }
    621                         } else {
    622                                 WP_CLI::log( "Skipping threshold check for {$wp_locale}, has existing language pack." );
    623                         }
    624 
    625                         // Check if new translations are available since last build.
    626                         if ( ! $this->force && isset( $existing_packs[ $wp_locale ] ) ) {
    627                                 $pack_time = strtotime( $existing_packs[ $wp_locale ]->updated );
    628                                 $glotpress_time = strtotime( $set->last_modified() );
    629 
    630                                 if ( $pack_time >= $glotpress_time ) {
    631                                         WP_CLI::log( "Skip {$wp_locale}, no new translations." );
    632                                         continue;
    633                                 }
    634                         }
    635 
    636                         $entries = GP::$translation->for_export( $data->gp_project, $set, [ 'status' => 'current' ] );
    637                         if ( ! $entries ) {
    638                                 WP_CLI::warning( "No current translations available for {$wp_locale}." );
    639                                 continue;
    640                         }
    641 
    642                         $working_directory = "{$data->svn_checkout}/{$data->domain}";
    643                         $export_directory  = "{$working_directory}/{$data->version}/{$wp_locale}";
    644                         $build_directory   = self::BUILD_DIR . "/{$data->type}s/{$data->domain}/{$data->version}";
    645 
    646                         $filename       = "{$data->domain}-{$wp_locale}";
    647                         $json_file_base = "{$export_directory}/{$filename}";
    648                         $po_file        = "{$export_directory}/{$filename}.po";
    649                         $mo_file        = "{$export_directory}/{$filename}.mo";
    650                         $zip_file       = "{$export_directory}/{$filename}.zip";
    651                         $build_zip_file = "{$build_directory}/{$wp_locale}.zip";
    652                         $build_sig_file = "{$build_zip_file}.sig";
    653 
    654                         // Update/create directories.
    655                         $this->update_svn_directory( $export_directory );
    656 
    657                         // Build a mapping based on where the translation entries occur and separate the po entries.
    658                         $mapping    = $this->build_mapping( $entries );
    659                         $po_entries = array_key_exists( 'po', $mapping ) ? $mapping['po'] : [];
    660 
    661                         unset( $mapping['po'] );
    662 
    663                         // Create JED json files for each JS file.
    664                         $json_files = $this->build_json_files( $data->gp_project, $gp_locale, $set, $mapping, $json_file_base );
    665 
    666                         // Create PO file.
    667                         $last_modified = $this->build_po_file( $data->gp_project, $gp_locale, $set, $po_entries, $po_file );
    668 
    669                         if ( is_wp_error( $last_modified ) ) {
    670                                 WP_CLI::warning( sprintf( "PO generation for {$wp_locale} failed: %s", $last_modified->get_error_message() ) );
    671 
    672                                 // Clean up.
    673                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    674 
    675                                 continue;
    676                         }
    677 
    678                         // Create MO file.
    679                         $result = $this->execute_command( sprintf(
    680                                 'msgfmt %s -o %s 2>&1',
    681                                 escapeshellarg( $po_file ),
    682                                 escapeshellarg( $mo_file )
    683                         ) );
    684 
    685                         if ( is_wp_error( $result ) ) {
    686                                 WP_CLI::error_multi_line( $result->get_error_data() );
    687                                 WP_CLI::warning( "MO generation for {$wp_locale} failed." );
    688 
    689                                 // Clean up.
    690                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    691 
    692                                 continue;
    693                         }
    694 
    695                         // Create ZIP file.
    696                         $result = $this->execute_command( sprintf(
    697                                 'zip -9 -j %s %s %s %s 2>&1',
    698                                 escapeshellarg( $zip_file ),
    699                                 escapeshellarg( $po_file ),
    700                                 escapeshellarg( $mo_file ),
    701                                 implode( ' ', array_map( 'escapeshellarg', $json_files ) )
    702                         ) );
    703 
    704                         if ( is_wp_error( $result ) ) {
    705                                 WP_CLI::error_multi_line( $result->get_error_data() );
    706                                 WP_CLI::warning( "ZIP generation for {$wp_locale} failed." );
    707 
    708                                 // Clean up.
    709                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    710 
    711                                 continue;
    712                         }
    713 
    714                         // Create build directories.
    715                         $result = $this->execute_command( sprintf(
    716                                 'mkdir -p %s 2>&1',
    717                                 escapeshellarg( $build_directory )
    718                         ) );
    719 
    720                         if ( is_wp_error( $result ) ) {
    721                                 WP_CLI::error_multi_line( $result->get_error_data() );
    722                                 WP_CLI::warning( "Creating build directories for {$wp_locale} failed." );
    723 
    724                                 // Clean up.
    725                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    726 
    727                                 continue;
    728                         }
    729 
    730                         // Move ZIP file to build directory.
    731                         $result = $this->execute_command( sprintf(
    732                                 'mv %s %s 2>&1',
    733                                 escapeshellarg( $zip_file ),
    734                                 escapeshellarg( $build_zip_file )
    735                         ) );
    736 
    737                         if ( is_wp_error( $result ) ) {
    738                                 WP_CLI::error_multi_line( $result->get_error_data() );
    739                                 WP_CLI::warning( "Moving ZIP file for {$wp_locale} failed." );
    740 
    741                                 // Clean up.
    742                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    743 
    744                                 continue;
    745                         }
    746 
    747                         // Generate a signature for the ZIP file.
    748                         if ( false && function_exists( 'wporg_sign_file' ) ) {
    749                                 $signatures = wporg_sign_file( $build_zip_file, 'translation' );
    750                                 if ( $signatures ) {
    751                                         file_put_contents( $build_sig_file, implode( "\n", $signatures ) );
    752                                 }
    753                         }
    754 
    755                         // Insert language pack into database.
    756                         $result = $this->insert_language_pack( $data->type, $data->domain, $wp_locale, $data->version, $last_modified );
    757 
    758                         if ( is_wp_error( $result ) ) {
    759                                 WP_CLI::warning( sprintf( "Language pack for {$wp_locale} failed: %s", $result->get_error_message() ) );
    760 
    761                                 // Clean up.
    762                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    763 
    764                                 continue;
    765                         }
    766 
    767                         // Add PO file to SVN.
    768                         $this->execute_command( sprintf(
    769                                 '%s add --quiet --force --parents %s 2>/dev/null',
    770                                 $svn_command,
    771                                 escapeshellarg( $po_file )
    772                         ) );
    773 
    774                         // Commit PO file.
    775                         $result = $this->execute_command( sprintf(
    776                                 '%s commit --quiet %s -m %s 2>&1',
    777                                 $svn_command,
    778                                 escapeshellarg( $data->svn_checkout ),
    779                                 escapeshellarg( "Update PO file for {$data->type} {$data->domain} {$data->version} {$wp_locale}." )
    780                         ) );
    781 
    782                         if ( is_wp_error( $result ) ) {
    783                                 WP_CLI::error_multi_line( $result->get_error_data() );
    784                                 WP_CLI::warning( "SVN commit for {$wp_locale} failed." );
    785 
    786                                 // Clean up.
    787                                 $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    788 
    789                                 continue;
    790                         }
    791 
    792                         // Clean up.
    793                         $this->execute_command( sprintf( 'rm -rf %s', escapeshellarg( $working_directory ) ) );
    794 
    795                         WP_CLI::success( "Language pack for {$wp_locale} generated." );
    796                 }
    797         }
    798225}
Note: See TracChangeset for help on using the changeset viewer.