register_global_scripts(); $this->register_scripts(); $this->register_styles(); } /** * Register scripts * * @return void */ public function register_scripts() { $scripts = $this->get_scripts(); foreach ( $scripts as $handle => $script ) { $deps = isset( $script['deps'] ) ? $script['deps'] : []; $in_footer = isset( $script['in_footer'] ) ? $script['in_footer'] : true; $version = isset( $script['version'] ) ? $script['version'] : $this->get_version( $script['src'] ); $deps = $this->get_dependencies( $script['src'], $deps ); if ( in_array( 'wp-i18n', $deps, true ) ) { $deps[] = 'booktics-i18n-loader'; } wp_register_script( $handle, $script['src'], $deps, $version, $in_footer ); if ( function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( $handle, 'booktics' ); } if ( 'booktics-packages' === $handle ) { wp_add_inline_script( 'booktics-packages', '(function(){try{if(window.bookticsPreloadedLocaleData && window.wp && window.wp.i18n && window.wp.i18n.setLocaleData){window.wp.i18n.setLocaleData(window.bookticsPreloadedLocaleData,"booktics");}}catch(e){}})();', 'before' ); } } } /** * Register global scripts. * * @return void */ private function register_global_scripts() { $scripts = [ 'booktics-i18n-loader' => [ 'src' => booktics()->assets_url . '/build/js/i18n-loader.js', 'deps' => [], 'in_footer' => true, ], ]; foreach ( $scripts as $handle => $script ) { $deps = isset( $script['deps'] ) ? $script['deps'] : []; $in_footer = isset( $script['in_footer'] ) ? $script['in_footer'] : true; $version = isset( $script['version'] ) ? $script['version'] : $this->get_version( $script['src'] ); $deps = $this->get_dependencies( $script['src'], $deps ); wp_register_script( $handle, $script['src'], $deps, $version, $in_footer ); } } /** * Enqueue i18n loader and set its state. * * @return void */ protected function enqueue_i18n_loader() { $data = [ 'baseUrl' => false, 'locale' => determine_locale(), 'domainMap' => [], 'domainPaths' => [], 'translationMap' => (object) [], 'preloaded' => false, ]; $lang_dir = WP_LANG_DIR; $content_dir = WP_CONTENT_DIR; $abspath = ABSPATH; if ( strpos( $lang_dir, $content_dir ) === 0 ) { $data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) ); } elseif ( strpos( $lang_dir, $abspath ) === 0 ) { $data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) ); } $locale_data = $this->get_preloaded_locale_data( $data['locale'] ); $data['preloaded'] = ! empty( $locale_data ); // The consolidated locale data already covers every string in the bundles, // so the per-chunk hash map is only needed when it could not be built. if ( ! $data['preloaded'] ) { $data['translationMap'] = (object) $this->get_translation_map(); } wp_enqueue_script( 'booktics-i18n-loader' ); $data['domainMap'] = (object) $data['domainMap']; $data['domainPaths'] = (object) $data['domainPaths']; $inline = '(function(){var k="bookticsI18nLoader"; if (typeof window[k] === "undefined") { window[k] = {}; } window[k].state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';'; if ( ! empty( $locale_data ) ) { $inline .= ' window[k].preloadedLocaleData = window.bookticsPreloadedLocaleData = ' . wp_json_encode( $locale_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . ';'; } $inline .= '})();'; wp_add_inline_script( 'booktics-i18n-loader', $inline, 'before' ); } /** * Register styles * * @return void */ public function register_styles() { $styles = $this->get_styles(); foreach ( $styles as $handle => $style ) { $deps = isset( $style['deps'] ) ? $style['deps'] : false; $version = booktics()->version; wp_register_style( $handle, $style['src'], $deps, $version ); } } /** * Get script and style file dependencies * * @param string $file_name * @param array $deps * * @return array */ private function get_dependencies( $file_name, $deps = [] ) { $assets = $this->get_file_assets( $file_name ); $assets_deps = ! empty( $assets['dependencies'] ) ? $assets['dependencies'] : []; $merged_deps = array_merge( $assets_deps, $deps ); return $merged_deps; } /** * Get file assets * * @param string $file_name * * @return array */ private function get_file_assets( $file_url ) { $file = $this->get_file_path( $file_url ); $assets = []; if ( file_exists( $file ) ) { $assets = include $file; } return $assets; } /** * Get script file version from .asset.php (generated by DependencyExtractionWebpackPlugin). * * @param string $file_name * * @return string */ private function get_version( $file_name ) { $assets = $this->get_file_assets( $file_name ); $assets_vers = ! empty( $assets['version'] ) ? $assets['version'] : booktics()->version; return $assets_vers; } /** * Get file path from url * * @param string $url * * @return string|false */ private function get_file_path( $url ) { if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { return false; } $url_parts = wp_parse_url( $url ); if ( ! isset( $url_parts['path'] ) ) { return false; } $path = $url_parts['path']; $path = preg_replace( '/\.js$/', '.asset.php', $path ); if ( isset( $_SERVER['DOCUMENT_ROOT'] ) ) { $file_path = sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $path; } else { $file_path = ''; } if ( ! file_exists( $file_path ) ) { return false; } return $file_path; } /** * Build the Jed locale data for every translated string in the text domain. * * Script translations are normally split into one JSON file per bundled * script, keyed by an md5 of that script's path. Async chunk filenames carry * a content hash and change on every build, so those JSON files go stale as * soon as the plugin is updated. Registering the whole domain up front keeps * the React apps translated regardless of how the bundles were chunked. * * @param string $locale Locale being rendered. * * @return array Jed formatted locale data, empty when there is nothing to preload. */ private function get_preloaded_locale_data( $locale ) { static $cache = []; if ( isset( $cache[ $locale ] ) ) { return $cache[ $locale ]; } $cache[ $locale ] = []; if ( 'en_US' === $locale ) { return $cache[ $locale ]; } if ( ! is_textdomain_loaded( 'booktics' ) ) { load_plugin_textdomain( 'booktics', false, dirname( plugin_basename( BOOKTICS_FILE ) ) . '/languages' ); } $translations = get_translations_for_domain( 'booktics' ); // WP_Translations (WP 6.5+, .l10n.php files) does not extend Translations, // so check for the entries it exposes rather than the class. if ( ! is_object( $translations ) || $translations instanceof \NOOP_Translations ) { return $cache[ $locale ]; } $entries = (array) $translations->entries; $messages = []; foreach ( $entries as $entry ) { if ( ! $entry instanceof \Translation_Entry || ! is_string( $entry->singular ) || '' === $entry->singular ) { continue; } $entry_translations = array_values( array_filter( (array) $entry->translations, function ( $translation ) { return is_string( $translation ) && '' !== $translation; } ) ); if ( empty( $entry_translations ) ) { continue; } // wp.i18n keys contextual strings as "contextsingular". $key = ( is_string( $entry->context ) && '' !== $entry->context ) ? $entry->context . "\4" . $entry->singular : $entry->singular; $messages[ $key ] = $entry_translations; } if ( empty( $messages ) ) { return $cache[ $locale ]; } $headers = (array) $translations->headers; $locale_data = [ '' => [ 'domain' => 'booktics', 'lang' => $locale, 'plural-forms' => ! empty( $headers['Plural-Forms'] ) ? $headers['Plural-Forms'] : 'nplurals=2; plural=n != 1;', ], ] + $messages; /** * Filter the locale data preloaded for the React apps. * * Useful to trim the payload down to the strings a site actually renders. * * @param array $locale_data Jed formatted locale data. * @param string $locale Locale being rendered. */ $cache[ $locale ] = apply_filters( 'booktics_preloaded_locale_data', $locale_data, $locale ); return $cache[ $locale ]; } /** * Get map of translation file hashes to their URLs. * * @return array Hash => URL map. */ private function get_translation_map() { $map = []; $locale = determine_locale(); $domain = 'booktics'; $locale_variants = array_unique( array_filter( [ $locale, str_replace( '-', '_', $locale ), str_replace( '_', '-', $locale ), ] ) ); foreach ( $this->get_translation_directories() as $dir ) { if ( ! is_dir( $dir['path'] ) || ! is_readable( $dir['path'] ) ) { continue; } foreach ( $locale_variants as $variant ) { $files = glob( $dir['path'] . $domain . '-' . $variant . '-*.json' ); if ( ! $files ) { continue; } foreach ( $files as $file ) { $filename = basename( $file ); $variant_prefix = $domain . '-' . $variant . '-'; if ( strpos( $filename, $variant_prefix ) === 0 ) { $hash = substr( $filename, strlen( $variant_prefix ), -5 ); if ( $hash ) { $map[ $hash ] = trailingslashit( $dir['url'] ) . $filename; } } } } } return $map; } /** * Return translation directory paths and their corresponding URLs. * * @return array */ private function get_translation_directories() { return [ [ 'path' => WP_CONTENT_DIR . '/languages/plugins/', 'url' => content_url( 'languages/plugins/' ), ], [ 'path' => WP_CONTENT_DIR . '/languages/loco/plugins/', 'url' => content_url( 'languages/loco/plugins/' ), ], [ 'path' => booktics()->plugin_directory . '/languages/', 'url' => booktics()->plugin_url . '/languages/', ], ]; } }