Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract map for i18n strings to use for directed load of i18n strings per view. #6051

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d43cc35
proof of concept webpack plugin to extract i18n strings into a chunk-…
nerrad Apr 6, 2018
d8a28c8
helper methods for getting translations for given chunks
nerrad Apr 6, 2018
b9afb52
styling and add missing i18n function to defaults
nerrad Apr 6, 2018
02e7686
add new class for handling setting up the wp.i18n.setLocaleData
nerrad Apr 7, 2018
c33a646
correct index details for translation and remove usage of domain argu…
nerrad Apr 9, 2018
25c83f9
make wepback 4 and webpack 3 compatible
nerrad Apr 10, 2018
30c4357
add translation-map.json to gitignore
nerrad Apr 11, 2018
fc7fdc6
refactor so we now inline `wp.i18n.setLocale` for each handle that ha…
nerrad Apr 13, 2018
1d5ace0
account for gutenberg domain using “default” domain in jed
nerrad Apr 13, 2018
d4449f0
don’t register scripts with `gutenberg` domain
nerrad Apr 13, 2018
9b7fc0f
filter and array_unique string set
nerrad Apr 13, 2018
c931e62
remove commented out code
nerrad Apr 13, 2018
7cab881
fix extractions for _x and _nx
nerrad Apr 13, 2018
c0a6e35
switch usage of has to native hasOwnProperty
nerrad Apr 13, 2018
ba76756
add script shortcuts for running phpcbf (fix phpcs linting errors)
nerrad Apr 13, 2018
788bc2f
fix lint errors/warnings
nerrad Apr 13, 2018
a7ea9fc
streamline generated map and logic to depend on handle name vs chunk
nerrad Apr 15, 2018
fbc625f
remove unnecessary multi-line function call
nerrad Apr 15, 2018
0dc8960
fix linting issues
nerrad Apr 15, 2018
aec2b13
update .gitignore
nerrad Apr 15, 2018
d31a497
tweak webpack plugin map entries are merged.
nerrad Apr 16, 2018
8bc06fc
declare variables at start of scope
nerrad Apr 17, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ languages/gutenberg.pot
phpcs.xml
yarn.lock
docker-compose.override.yml
translation-map.json
130 changes: 130 additions & 0 deletions bin/i18n-map-extractor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const recast = require( 'recast' );
const { forEach, reduce, includes, has, isEmpty, isFunction } = require( 'lodash' );
const { writeFileSync } = require( 'fs' );
const NormalModule = require( 'webpack/lib/NormalModule' );

class wpi18nExtractor {
constructor( options ) {
const DEFAULT_FUNCTIONS = [
'__',
'_n',
'_x',
'_nx',
'sprintf',
];
this.options = options || {};
this.options.filename = this.options.filename || 'translation-map.json';
this.translationMap = {};
this.functionNames = this.options.functionNames || DEFAULT_FUNCTIONS;
}

extractStringFromFunctionCall( args, functionName ) {
const strings = [];
switch ( functionName ) {
case '__':
case 'sprintf':
case '_n':
strings.push( args[ 0 ].value );
break;
case '_x':
strings.push( args[ 1 ].value + '\u0004' + args[ 0 ].value );
break;
case '_nx':
strings.push( args[ 3 ].value + '\u0004' + args[ 0 ].value );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had noticed that the msgID format for _x and _nx strings is {context}\u0004{singular_string} so I corrected that. This took care of a few strings that weren't translating in my tests.

}
return strings;
}

getStringsFromModule( module, extractor ) {
const source = module.originalSource().source();
if ( isEmpty( source ) ) {
return [];
}
let strings = [];
try {
const ast = recast.parse( source );
const { types } = recast;
types.visit( ast, {
visitCallExpression: function( path ) {
const node = path.node;
if ( includes( extractor.functionNames, node.callee.name ) &&
node.arguments
) {
strings = strings.concat(
extractor.extractStringFromFunctionCall(
types.getFieldValue( node, 'arguments' ),
node.callee.name,
)
);
}
this.traverse( path );
},
} );
} catch ( e ) {
//we just want to skip parsing errors.
}
return strings;
}

parseSourcesToMap( modules, chunkName, extractor ) {
const { getStringsFromModule } = extractor;
reduce(
Array.from( modules ),
function( mapped, module ) {
if ( ! ( module instanceof NormalModule ) ||
! isFunction( module.originalSource )
) {
return mapped;
}
if ( ! has( mapped, chunkName ) ) {
mapped[ chunkName ] = [];
}
mapped[ chunkName ] = mapped[ chunkName ]
.concat( getStringsFromModule( module, extractor ) );
return mapped;
},
extractor.translationMap
);
}

apply( compiler ) {
const { processChunks } = this;
const extractor = this;

/**
* webpack 4 registration
*/
if ( has( compiler, 'hooks' ) ) {
compiler.hooks.thisCompilation.tap( 'webpack-i18n-map-extractor', compilation => {
compilation.hooks.optimizeChunks.tap( 'webpack-i18n-map-extractor', chunks => {
processChunks( chunks, extractor );
} );
} );
} else {
compiler.plugin( 'this-compilation', ( compilation ) => {
compilation.plugin( [ 'optimize-chunks', 'optimize-extracted-chunks' ], ( chunks ) => {
processChunks( chunks, extractor );
} );
} );
}
}

processChunks( chunks, extractor ) {
const {
options,
translationMap,
parseSourcesToMap,
} = extractor;
forEach( chunks, function( chunk ) {
if ( chunk.name ) {
parseSourcesToMap( chunk._modules, chunk.name, extractor );
}
} );
writeFileSync( './' + options.filename,
JSON.stringify( translationMap, null, 2 ),
'utf-8'
);
}
}

module.exports = wpi18nExtractor;
198 changes: 198 additions & 0 deletions lib/class-gb-scripts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

if ( ! defined( 'ABSPATH' ) ) {
die( 'Silence is golden.' );
}

/**
* This class handles queueing up only the translations for javascript files that have been enqueued for translation.
*
* @since 1.0.0
*/
class GB_Scripts {

/**
* Will hold all registered i18n scripts.
*
* @var array
*/
private $registered_i18n = array();


/**
* Used to hold queued translations for the chunks loading in a view.
*
* @var array
*/
private $queued_chunk_translations = array();


/**
* Obtained from the generated json file from the all javascript using wp.i18n with a map of chunk names to
* translation strings.
*
* @var array
*/
private $chunk_map;


/**
* GB_Scripts constructor.
*
* @param array() $chunk_map An array of chunks and the strings translated for those chunks. If not provided class
* will look for map in root of plugin with filename of 'translation-map.json'.
*/
public function __construct( $chunk_map = array() ) {
$this->set_chunk_map( $chunk_map );
add_filter( 'print_scripts_array', array( $this, 'queue_i18n' ) );
}


/**
* Used to register a script that has i18n strings for its $chunk_name
*
* @param string $handle
* @param string $chunk_name
* @param string $domain
*/
public function register_script_i18n( $handle, $chunk_name, $domain ) {
$this->registered_i18n[ $handle ] = array( $chunk_name, $domain );
}


/**
* Callback on print_scripts_array to listen for scripts enqueued and handle seting up the localized data.
*
* @param $handles
*
* @return array
*/
public function queue_i18n( $handles ) {
if ( empty( $this->registered_i18n ) || empty( $this->chunk_map ) ) {
return $handles;
}
foreach ( ( array ) $handles as $handle ) {
$this->queue_i18n_chunk_for_handle( $handle );
}
if ( $this->queued_chunk_translations ) {
foreach ( $this->queued_chunk_translations as $handle => $translations_for_domain ) {
$this->register_inline_script(
$handle,
$translations_for_domain[ 'translations' ],
$translations_for_domain[ 'domain' ]
);
}
}
return $handles;
}


/**
* Registers inline script with translations for given handle and domain.
*
* @param string $handle Handle used to register javascript file containing translations
* @param array $translations
* @param string $domain Domain for translations. If left empty then strings are registered with the default
* domain for the javascript.
*/
private function register_inline_script( $handle, $translations, $domain = '' ) {
$script = $domain ?
'wp.i18n.setLocaleData( ' . json_encode( $translations ) . ', ' . $domain . ' );' :
'wp.i18n.setLocaleData( ' . json_encode( $translations ) . ' );';
wp_add_inline_script(
$handle,
$script,
'before'
);
}


/**
* Queues up the translation strings for the given handle.
*
* @param string $handle
*/
private function queue_i18n_chunk_for_handle( $handle ) {
if ( isset( $this->registered_i18n[ $handle ] ) ) {
list( $chunk, $domain ) = $this->registered_i18n[ $handle ];
$translations = $this->get_jed_locale_data_for_domain_and_chunk( $chunk, $domain );
if ( count( $translations ) > 1 ) {
$this->queued_chunk_translations[ $handle ] = array(
'domain' => $domain,
'translations' => $this->get_jed_locale_data_for_domain_and_chunk( $chunk, $domain )
);
}
unset ( $this->registered_i18n[ $handle ] );
}
}


/**
* Sets the internal chunk_map property.
*
* If $chunk_map is empty or not an array, will attempt to load a chunk map from a default named map.
*
* @param array $chunk_map
*/
private function set_chunk_map( $chunk_map ) {
if ( empty( $chunk_map ) || ! is_array( $chunk_map) ) {
$chunk_map = json_decode(
file_get_contents( gutenberg_dir_path() . 'translation-map.json' ),
true
);
}
$this->chunk_map = $chunk_map;
}


/**
* Get the jed locale data for a given chunk and domain
*
* @param string $chunk
* @param string $domain
*
* @return array()
*/
protected function get_jed_locale_data_for_domain_and_chunk( $chunk, $domain ) {
$translations = gutenberg_get_jed_locale_data( $domain );
//get index for adding back after extracting strings for this $chunk
$index = $translations[ '' ];
$translations = $this->get_locale_data_matching_map(
$this->get_original_strings_for_chunk_from_map( $chunk ),
$translations
);
$translations[ '' ] = $index;
return $translations;
}


/**
* Get locale data for given strings from given translations
*
* @param $string_set
* @param $translations
*
* @return array
*/
protected function get_locale_data_matching_map( $string_set, $translations ) {
if ( ! is_array( $string_set ) || ! is_array( $translations ) || empty ( $string_set ) ) {
return array();
}
//some strings with quotes in them will break on the array_flip, so making sure quotes in the string are slashed
//also filter falsey values
$string_set = array_unique( array_filter( wp_slash( $string_set ) ) );
return array_intersect_key( $translations, array_flip( $string_set ) );
}


/**
* Get original strings to translate for the given chunk from the map
*
* @param string $chunk_name
*
* @return array
*/
protected function get_original_strings_for_chunk_from_map( $chunk_name ) {
return isset( $this->chunk_map[ $chunk_name ] ) ? $this->chunk_map[ $chunk_name ] : array();
}
}
29 changes: 22 additions & 7 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function gutenberg_get_script_polyfill( $tests ) {
*/
function gutenberg_register_scripts_and_styles() {
gutenberg_register_vendor_scripts();
gutenberg_register_wpi18n_for_scripts();

// Editor Scripts.
wp_register_script(
Expand Down Expand Up @@ -800,6 +801,27 @@ function gutenberg_capture_code_editor_settings( $settings ) {
return false;
}


function gutenberg_register_wpi18n_for_scripts() {
$handles_and_chunks = array(
'wp-data' => 'data',
'wp-core-data' => 'coreData',
'wp-utils' => 'utils',
'wp-hooks' => 'hooks',
'wp-date' => 'date',
'wp-element' => 'element',
'wp-components' => 'components',
'wp-blocks' => 'blocks',
'wp-viewport' => 'viewport',
'wp-editor' => 'editor',
'wp-edit-post' => 'editPost',
'wp-plugins' => 'plugins'
);
foreach ( $handles_and_chunks as $handle => $chunk ) {
gutenberg_register_script_i18n( $handle, $chunk );
}
}

/**
* Scripts & Styles.
*
Expand Down Expand Up @@ -882,13 +904,6 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
);
}

// Prepare Jed locale data.
$locale_data = gutenberg_get_jed_locale_data( 'gutenberg' );
wp_add_inline_script(
'wp-i18n',
'wp.i18n.setLocaleData( ' . json_encode( $locale_data ) . ' );'
);

// Preload server-registered block schemas.
wp_localize_script( 'wp-blocks', '_wpBlocks', gutenberg_prepare_blocks_for_js() );

Expand Down
Loading