-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Closed
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 d8a28c8
helper methods for getting translations for given chunks
nerrad b9afb52
styling and add missing i18n function to defaults
nerrad 02e7686
add new class for handling setting up the wp.i18n.setLocaleData
nerrad c33a646
correct index details for translation and remove usage of domain argu…
nerrad 25c83f9
make wepback 4 and webpack 3 compatible
nerrad 30c4357
add translation-map.json to gitignore
nerrad fc7fdc6
refactor so we now inline `wp.i18n.setLocale` for each handle that ha…
nerrad 1d5ace0
account for gutenberg domain using “default” domain in jed
nerrad d4449f0
don’t register scripts with `gutenberg` domain
nerrad 9b7fc0f
filter and array_unique string set
nerrad c931e62
remove commented out code
nerrad 7cab881
fix extractions for _x and _nx
nerrad c0a6e35
switch usage of has to native hasOwnProperty
nerrad ba76756
add script shortcuts for running phpcbf (fix phpcs linting errors)
nerrad 788bc2f
fix lint errors/warnings
nerrad a7ea9fc
streamline generated map and logic to depend on handle name vs chunk
nerrad fbc625f
remove unnecessary multi-line function call
nerrad 0dc8960
fix linting issues
nerrad aec2b13
update .gitignore
nerrad d31a497
tweak webpack plugin map entries are merged.
nerrad 8bc06fc
declare variables at start of scope
nerrad File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,4 @@ languages/gutenberg.pot | |
phpcs.xml | ||
yarn.lock | ||
docker-compose.override.yml | ||
translation-map.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | ||
} | ||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.