Making WordPress.org

Changeset 11122


Ignore:
Timestamp:
07/19/2021 03:13:43 AM (4 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.