/** * `load_script_translation_file` filter callback * Alternative method to merging in `pre_load_script_translations` * @param string|false $path candidate JSON file (false on final attempt) * @param string $handle * @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) ) ){ $system = loco_constant('WP_LANG_DIR').'/'; $custom = loco_constant('LOCO_LANG_DIR').'/'; if( str_starts_with($path,$system) ){ $mapped = substr_replace($path,$custom,0,strlen($system) ); // Defer merge until either JSON is resolved or final attempt passes an empty path. if( is_readable($mapped) ){ $this->json[$handle] = $mapped; } } } // 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 $json contents of JSON file that WordPress has read * @param string $path path relating to given JSON (not used here) * @param string $handle script handle for registered merge * @return string final JSON translations * @noinspection PhpUnusedParameterInspection */ 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 $json Original/fallback JSON * @param string $custom 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'] = [ '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[] $jed * @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']; } }