File: /var/www/ideasforum.kaunokolegija.lt/wp-content/plugins/loco-translate/src/hooks/LoadHelper.php
<?php /** @noinspection PhpUnusedParameterInspection */
/**
* Text Domain loading helper.
* Ensures custom translations can be loaded from `wp-content/languages/loco`.
* This functionality is optional. You can disable the plugin if you're not loading MO or JSON files from languages/loco
*/
class Loco_hooks_LoadHelper extends Loco_hooks_Hookable {
/**
* theme/plugin text domain loading context in progress
* @var string[] [ $subdir, $domain, $locale ]
*/
private $context;
/**
* Protects against recursive calls to load_textdomain()
* @var bool[]
*/
private $lock;
/**
* Custom/safe directory path with trailing slash
* @var string
*/
private $base;
/**
* Locations that can be mapped to equivalent paths under custom directory
* @var array[]
*/
private $map;
/**
* Deferred JSON files under our custom directory, indexed by script handle
* @var string[]
*/
private $json;
/**
* {@inheritDoc}
*/
public function __construct(){
parent::__construct();
$this->lock = array();
$this->json = array();
$this->base = trailingslashit( loco_constant('LOCO_LANG_DIR') );
// add system locations which have direct equivalent custom/safe locations under LOCO_LANG_DIR
// not adding theme paths because as long as load_theme_textdomain is used they will be mapped by context.
$this->add('', loco_constant('WP_LANG_DIR') )
->add('plugins/', loco_constant('WP_PLUGIN_DIR') )
->add('plugins/', loco_constant('WPMU_PLUGIN_DIR') );
}
/**
* Add a mappable location
* @param string
* @param string
* @return self
*/
private function add( $subdir, $path ){
if( $path ){
$path = trailingslashit($path);
$this->map[] = array( $subdir, $path, strlen($path) );
}
return $this;
}
/**
* Map a file directly from a standard system location to LOCO_LANG_DIR.
* - this does not check if file exists, only what the path should be.
* - this does not handle filename differences (so won't work with themes)
* @param string e.g. {WP_CONTENT_DIR}/languages/plugins/foo or {WP_PLUGIN_DIR}/foo/anything/foo
* @return string e.g. {WP_CONTENT_DIR}/languages/loco/plugins/foo
*/
private function resolve( $path ){
foreach( $this->map as $key => $data ){
list($subdir,$prefix,$len) = $data;
if( substr($path,0,$len) === $prefix ){
if( '' === $subdir ){
return $this->base.substr($path,$len);
}
return $this->base.$subdir.basename($path);
}
}
return '';
}
/**
* `theme_locale` filter callback.
* Signals the beginning of a "load_theme_textdomain" process
* @param string
* @param string
* @return string
*/
public function filter_theme_locale( $locale, $domain = '' ){
$this->context = array( 'themes/', $domain, $locale );
unset( $this->lock[$domain] );
return $locale;
}
/**
* `plugin_locale` filter callback.
* Signals the beginning of a "load_plugin_textdomain" process
* @param string
* @param string
* @return string
*/
public function filter_plugin_locale( $locale, $domain = '' ){
$this->context = array( 'plugins/', $domain, $locale );
unset( $this->lock[$domain] );
return $locale;
}
/**
* `unload_textdomain` action callback.
* Lets us release lock so that custom file may be loaded again (hopefully for another locale)
* @param string
* @return void
*/
public function on_unload_textdomain( $domain ){
unset( $this->lock[$domain] );
}
/**
* `load_textdomain` action callback.
* Lets us load our custom translations before WordPress loads what it was going to anyway.
* We're deliberately not stopping WordPress loading $mopath, if it exists it will be merged on top of our custom strings.
* @param string
* @param string
* @return void
*/
public function on_load_textdomain( $domain, $mopath ){
$key = '';
// domains may be split into multiple files
$name = pathinfo( $mopath, PATHINFO_FILENAME );
if( $lpos = strrpos( $name, '-') ){
$slug = substr( $name, 0, $lpos );
if( $slug !== $domain ){
$key = $slug;
}
}
// avoid recursion when we've already handled this domain/slug
if( isset($this->lock[$domain][$key]) ){
return;
}
// if context is set, then a theme or plugin initialized the loading process properly
if( is_array($this->context) ){
list( $subdir, $_domain, $locale ) = $this->context;
$this->context = null;
if( $_domain !== $domain ){
return;
}
$mopath = $this->base.$subdir.$domain.'-'.$locale.'.mo';
}
// else load_textdomain must have been called directly, including to load core domain
else {
$mopath = $this->resolve($mopath);
if( '' === $mopath ){
return;
}
}
// Load our custom translations avoiding recursion back into this hook
$this->lock[$domain][$key] = true;
load_textdomain( $domain, $mopath );
}
/*
* `load_script_translation_file` filter callback
* Alternative method to merging in `pre_load_script_translations`
* @param string|false candidate JSON file (false on final attempt)
* @param string
* @return string
*/
public function filter_load_script_translation_file( $path = '', $handle = '' ){
// currently handle-based JSONs for author-provided translations will never map.
if( is_string($path) && preg_match('/^-[a-f0-9]{32}\\.json$/',substr($path,-38) ) ){
$custom = $this->resolve($path);
if( $custom && is_readable($custom) ){
// Defer until either JSON is resolved or final attempt passes an empty path.
$this->json[$handle] = $custom;
}
}
// If we return an unreadable file, load_script_translations will not fire.
// However, we need to allow WordPress to try all files. Last attempt will have empty path
else if( false === $path && array_key_exists($handle,$this->json) ){
$path = $this->json[$handle];
unset( $this->json[$handle] );
}
return $path;
}
/**
* `load_script_translations` filter callback.
* Merges custom translations on top of installed ones, as late as possible.
* @param string contents of JSON file that WordPress has read
* @param string path relating to given JSON (not used here)
* @param string script handle for registered merge
* @return string final JSON translations
*/
public function filter_load_script_translations( $json = '', $path = '', $handle = '' ){
if( array_key_exists($handle,$this->json) ){
$path = $this->json[$handle];
unset( $this->json[$handle] );
$json = self::mergeJson( $json, file_get_contents($path) );
}
return $json;
}
/**
* Merge two JSON translation files such that custom strings override
* @param string Original/fallback JSON
* @param string Custom JSON (must exclude empty keys)
* @return string Merged JSON
*/
private static function mergeJson( $json, $custom ){
$fallbackJed = json_decode($json,true);
$overrideJed = json_decode($custom,true);
if( self::jedValid($fallbackJed) && self::jedValid($overrideJed) ){
// Original key is probably "messages" instead of domain, but this could change at any time.
// Although custom file should have domain key, there's no guarantee JSON wasn't overwritten or key changed.
$overrideMessages = current($overrideJed['locale_data']);
$fallbackMessages = current($fallbackJed['locale_data']);
// We could merge headers, but custom file should be correct
// $overrideMessages[''] += $fallbackMessages[''];
// Continuing to use "messages" here as per WordPress. Good backward compatibility is likely.
// Note that our custom JED is sparse (exported with empty keys absent). This is essential for + operator.
$overrideJed['locale_data'] = array (
'messages' => $overrideMessages + $fallbackMessages,
);
// Note that envelope will be the custom one. No functional difference but demonstrates that merge worked.
$overrideJed['merged'] = true;
$json = json_encode($overrideJed);
}
// Handle situations where one or neither JSON strings are valid
else if( self::jedValid($overrideJed) ){
$json = $custom;
}
else if( ! self::jedValid($fallbackJed) ){
$json = '';
}
return $json;
}
/**
* Test if unserialized JSON is a valid JED structure
* @param array
* @return bool
*/
private static function jedValid( $jed ){
return is_array($jed) && array_key_exists('locale_data',$jed) && is_array($jed['locale_data']) && $jed['locale_data'];
}
/*
* Alternative merging method using `script_loader_tag` filter callback.
* We could load two JSONs via two calls to wp.i18n.setLocaleData BUT WordPress closure makes it difficult/unreliable.
* @param string candidate JSON file
* @param string
* @param string
* @return string
*
public function filter_script_loader_tag( $tag = '', $handle = '', $src = '' ){
if( array_key_exists($handle,$this->json) ){
$json = file_get_contents($this->json[$handle] );
unset($this->json[$handle]);
// splice custom translations between original ones and the script they're attached to.
list( $foo, $bar ) = explode('</script>',$tag,2);
$tag = $foo."\n console.log({$json});</script>".$bar;
}
return $tag;
}*/
}