* : Path to an existing PO file or a directory containing multiple PO files. * * [] * : Path to the destination directory for the resulting JSON files. Defaults to the source directory. * * [--purge] * : Whether to purge the strings that were extracted from the original source file. Defaults to true, use `--no-purge` to skip the removal. * * [--update-mo-files] * : Whether MO files should be updated as well after updating PO files. * Only has an effect when used in combination with `--purge`. * * [--pretty-print] * : Pretty-print resulting JSON files. * * [--use-map=] * : Whether to use a mapping file for the strings, as a JSON value, array to specify multiple. * Each element can either be a string (file path) or object (map). * * ## EXAMPLES * * # Create JSON files for all PO files in the languages directory * $ wp i18n make-json languages * * # Create JSON files for my-plugin-de_DE.po and leave the PO file untouched. * $ wp i18n make-json my-plugin-de_DE.po /tmp --no-purge * * # Create JSON files with mapping * $ wp i18n make-json languages --use-map=build/map.json * * # Create JSON files with multiple mappings * $ wp i18n make-json languages '--use-map=["build/map.json","build/map2.json"]' * * # Create JSON files with object mapping * $ wp i18n make-json languages '--use-map={"source/index.js":"build/index.js"}' * * @when before_wp_load * * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { $assoc_args = Utils\parse_shell_arrays( $assoc_args, array( 'use-map' ) ); $purge = Utils\get_flag_value( $assoc_args, 'purge', true ); $update_mo_files = Utils\get_flag_value( $assoc_args, 'update-mo-files', true ); $map_paths = Utils\get_flag_value( $assoc_args, 'use-map', false ); if ( Utils\get_flag_value( $assoc_args, 'pretty-print', false ) ) { $this->json_options |= JSON_PRETTY_PRINT; } $source = realpath( $args[0] ); if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) { WP_CLI::error( 'Source file or directory does not exist.' ); } $destination = is_file( $source ) ? dirname( $source ) : $source; if ( isset( $args[1] ) ) { $destination = $args[1]; } $map = $this->build_map( $map_paths ); if ( is_array( $map ) && empty( $map ) ) { WP_CLI::error( 'No valid keys found. No file was created.' ); } if ( ! is_dir( $destination ) && ! mkdir( $destination, 0777, true ) ) { WP_CLI::error( 'Could not create destination directory.' ); } $result_count = 0; if ( is_file( $source ) ) { $files = [ new SplFileInfo( $source ) ]; } else { $files = new IteratorIterator( new DirectoryIterator( $source ) ); } /** @var DirectoryIterator $file */ foreach ( $files as $file ) { if ( $file->isFile() && $file->isReadable() && 'po' === $file->getExtension() ) { $result = $this->make_json( $file->getRealPath(), $destination, $map ); $result_count += count( $result ); if ( $purge ) { $removed = $this->remove_js_strings_from_po_file( $file->getRealPath() ); if ( ! $removed ) { WP_CLI::warning( sprintf( 'Could not update file %s', basename( $source ) ) ); continue; } if ( $update_mo_files ) { $file_basename = basename( $file->getFilename(), '.po' ); $destination_file = "{$destination}/{$file_basename}.mo"; $translations = Translations::fromPoFile( $file->getPathname() ); if ( ! $translations->toMoFile( $destination_file ) ) { WP_CLI::warning( "Could not create file {$destination_file}" ); } } } } } WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) ); } /** * Collect maps from paths, normalize and merge * * @param string|array|bool $paths_or_maps argument. False to do nothing. * @return array|null Mapping array. Null if no maps specified. */ protected function build_map( $paths_or_maps ) { if ( false === $paths_or_maps ) { return null; } $map = []; // not an array: single value could also be object (associative array) if ( ! is_array( $paths_or_maps ) || empty( array_filter( array_keys( $paths_or_maps ), 'is_int' ) ) ) { $paths_or_maps = [ $paths_or_maps ]; } $paths = array_filter( $paths_or_maps, 'is_string' ); WP_CLI::debug( sprintf( 'Using %d map files: %s', count( $paths ), implode( ', ', $paths ) ), 'make-json' ); $maps = array_filter( $paths_or_maps, 'is_array' ); WP_CLI::debug( sprintf( 'Using %d inline map objects', count( $maps ) ), 'make-json' ); WP_CLI::debug( sprintf( 'Dropping %d invalid values from map argument', count( $paths_or_maps ) - count( $paths ) - count( $maps ) ), 'make-json' ); $to_transform = array_map( static function ( $value, $index ) { return [ $value, sprintf( 'inline object %d', $index ) ]; }, $maps, array_keys( $maps ) ); foreach ( $paths as $path ) { if ( ! file_exists( $path ) || is_dir( $path ) ) { WP_CLI::warning( sprintf( 'Map file %s does not exist', $path ) ); continue; } $json = json_decode( file_get_contents( $path ), true ); if ( ! is_array( $json ) ) { WP_CLI::warning( sprintf( 'Map file %s invalid', $path ) ); continue; } $to_transform[] = [ $json, $path ]; } foreach ( $to_transform as $transform ) { list( $json, $file ) = $transform; $key_num = count( $json ); // normalize contents to string[] $json = array_map( static function ( $value ) { if ( is_array( $value ) ) { $value = array_values( array_filter( $value, 'is_string' ) ); if ( ! empty( $value ) ) { return $value; } } if ( is_string( $value ) ) { return [ $value ]; } return null; }, $json ); WP_CLI::debug( sprintf( 'Dropped %d keys from %s', count( $json ) - $key_num, $file ), 'make-json' ); $map = array_merge_recursive( $map, $json ); } return $map; } /** * Splits a single PO file into multiple JSON files. * * @param string $source_file Path to the source file. * @param string $destination Path to the destination directory. * @param array|null $map Source to build file mapping. * @return array List of created JSON files. */ protected function make_json( $source_file, $destination, $map ) { /** @var Translations[] $mapping */ $mapping = []; $translations = new Translations(); $result = []; PoExtractor::fromFile( $source_file, $translations ); $base_file_name = basename( $source_file, '.po' ); $domain = $translations->getDomain(); if ( $domain && 0 !== strpos( $base_file_name, $domain ) ) { $base_file_name = "{$domain}-{$base_file_name}"; } foreach ( $translations as $translation ) { /** @var Translation $translation */ // Find all unique sources this translation originates from. $sources = array_map( static function ( $reference ) { $file = $reference[0]; if ( substr( $file, - 7 ) === '.min.js' ) { return substr( $file, 0, - 7 ) . '.js'; } if ( substr( $file, - 3 ) === '.js' ) { return $file; } return null; }, $this->reference_map( $translation->getReferences(), $map ) ); $sources = array_unique( array_filter( $sources ) ); foreach ( $sources as $source ) { if ( ! isset( $mapping[ $source ] ) ) { $mapping[ $source ] = new Translations(); // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Provide code that is meant to be used once the bug is fixed. // See https://core.trac.wordpress.org/ticket/45441 // $mapping[ $source ]->setDomain( $translations->getDomain() ); $mapping[ $source ]->setHeader( 'Language', $translations->getLanguage() ); $mapping[ $source ]->setHeader( 'PO-Revision-Date', $translations->getHeader( 'PO-Revision-Date' ) ); $plural_forms = $translations->getPluralForms(); if ( $plural_forms ) { list( $count, $rule ) = $plural_forms; $mapping[ $source ]->setPluralForms( $count, $rule ); } } $mapping[ $source ][] = $translation; } } $result += $this->build_json_files( $mapping, $base_file_name, $destination ); return $result; } /** * Takes the references and applies map, if given * * @param array $references translation references * @param array|null $map file mapping * @return array mapped references */ protected function reference_map( $references, $map ) { if ( is_null( $map ) ) { return $references; } // translate using map $temp = array_map( static function ( $reference ) use ( &$map ) { $file = $reference[0]; if ( array_key_exists( $file, $map ) ) { return $map[ $file ]; } return null; }, $references ); // this is now an array of arrays of sources, translate to array of sources $references = []; foreach ( $temp as $sources ) { if ( is_null( $sources ) ) { continue; } array_push( $references, ...$sources ); } // and wrap to array return array_map( static function ( $value ) { return [ $value ]; }, $references ); } /** * Builds a mapping of JS file names to translation entries. * * Exports translations for each JS file to a separate translation file. * * @param array $mapping A mapping of files to translation entries. * @param string $base_file_name Base file name for JSON files. * @param string $destination Path to the destination directory. * * @return array List of created JSON files. */ protected function build_json_files( $mapping, $base_file_name, $destination ) { $result = []; foreach ( $mapping as $file => $translations ) { /** @var Translations $translations */ $hash = md5( $file ); $destination_file = "{$destination}/{$base_file_name}-{$hash}.json"; $success = JedGenerator::toFile( $translations, $destination_file, [ 'json' => $this->json_options, 'source' => $file, ] ); if ( ! $success ) { WP_CLI::warning( sprintf( 'Could not create file %s', basename( $destination_file, '.json' ) ) ); continue; } $result[] = $destination_file; } return $result; } /** * Removes strings from PO file that only occur in JavaScript file. * * @param string $source_file Path to the PO file. * @return bool True on success, false otherwise. */ protected function remove_js_strings_from_po_file( $source_file ) { /** @var Translations[] $mapping */ $translations = new Translations(); PoExtractor::fromFile( $source_file, $translations ); foreach ( $translations->getArrayCopy() as $translation ) { /** @var Translation $translation */ if ( ! $translation->hasReferences() ) { continue; } foreach ( $translation->getReferences() as $reference ) { $file = $reference[0]; if ( substr( $file, - 3 ) !== '.js' ) { continue 2; } } unset( $translations[ $translation->getId() ] ); } return PoGenerator::toFile( $translations, $source_file ); } }