| @@ -1,7 +1,9 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | namespace Booktics\Assets; |
| 3 | 3 | |
| 4 | +if ( ! defined( 'ABSPATH' ) ) exit; | |
| 5 | + | |
| 4 | 6 | use Booktics\Contracts\Hookable_Service_Contract; |
| 5 | 7 | |
| 6 | 8 | /** |
| 7 | 9 | * Main assets management class |
| @@ -26,8 +28,9 @@ | ||
| 26 | 28 | * |
| 27 | 29 | * @return void |
| 28 | 30 | */ |
| 29 | 31 | public function register_styles_scripts() { |
| 32 | + $this->register_global_scripts(); | |
| 30 | 33 | $this->register_scripts(); |
| 31 | 34 | $this->register_styles(); |
| 32 | 35 | } |
| 33 | 36 | |
| @@ -33,10 +36,8 @@ | ||
| 33 | 36 | |
| 34 | 37 | /** |
| 35 | 38 | * Register scripts |
| 36 | 39 | * |
| 37 | - * @param array $scripts | |
| 38 | - * | |
| 39 | 40 | * @return void |
| 40 | 41 | */ |
| 41 | 42 | public function register_scripts() { |
| 42 | 43 | $scripts = $this->get_scripts(); |
| @@ -47,17 +48,115 @@ | ||
| 47 | 48 | $version = isset( $script['version'] ) ? $script['version'] : $this->get_version( $script['src'] ); |
| 48 | 49 | |
| 49 | 50 | $deps = $this->get_dependencies( $script['src'], $deps ); |
| 50 | 51 | |
| 52 | + if ( in_array( 'wp-i18n', $deps, true ) ) { | |
| 53 | + $deps[] = 'booktics-i18n-loader'; | |
| 54 | + } | |
| 55 | + | |
| 51 | 56 | wp_register_script( $handle, $script['src'], $deps, $version, $in_footer ); |
| 57 | + | |
| 58 | + if ( function_exists( 'wp_set_script_translations' ) ) { | |
| 59 | + wp_set_script_translations( $handle, 'booktics' ); | |
| 60 | + } | |
| 61 | + | |
| 62 | + if ( 'booktics-packages' === $handle ) { | |
| 63 | + wp_add_inline_script( | |
| 64 | + 'booktics-packages', | |
| 65 | + '(function(){try{if(window.bookticsPreloadedLocaleData && window.wp && window.wp.i18n && window.wp.i18n.setLocaleData){window.wp.i18n.setLocaleData(window.bookticsPreloadedLocaleData,"booktics");}}catch(e){}})();', | |
| 66 | + 'before' | |
| 67 | + ); | |
| 68 | + } | |
| 52 | 69 | } |
| 53 | 70 | } |
| 54 | 71 | |
| 55 | 72 | /** |
| 73 | + * Register global scripts. | |
| 74 | + * | |
| 75 | + * @return void | |
| 76 | + */ | |
| 77 | + private function register_global_scripts() { | |
| 78 | + $scripts = [ | |
| 79 | + 'booktics-i18n-loader' => [ | |
| 80 | + 'src' => booktics()->assets_url . '/build/js/i18n-loader.js', | |
| 81 | + 'deps' => [], | |
| 82 | + 'in_footer' => true, | |
| 83 | + ], | |
| 84 | + ]; | |
| 85 | + | |
| 86 | + foreach ( $scripts as $handle => $script ) { | |
| 87 | + $deps = isset( $script['deps'] ) ? $script['deps'] : []; | |
| 88 | + $in_footer = isset( $script['in_footer'] ) ? $script['in_footer'] : true; | |
| 89 | + $version = isset( $script['version'] ) ? $script['version'] : $this->get_version( $script['src'] ); | |
| 90 | + | |
| 91 | + $deps = $this->get_dependencies( $script['src'], $deps ); | |
| 92 | + | |
| 93 | + wp_register_script( $handle, $script['src'], $deps, $version, $in_footer ); | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * Enqueue i18n loader and set its state. | |
| 99 | + * | |
| 100 | + * @return void | |
| 101 | + */ | |
| 102 | + protected function enqueue_i18n_loader() { | |
| 103 | + $data = [ | |
| 104 | + 'baseUrl' => false, | |
| 105 | + 'locale' => determine_locale(), | |
| 106 | + 'domainMap' => [], | |
| 107 | + 'domainPaths' => [], | |
| 108 | + 'translationMap' => (object) [], | |
| 109 | + 'preloaded' => false, | |
| 110 | + ]; | |
| 111 | + | |
| 112 | + $lang_dir = WP_LANG_DIR; | |
| 113 | + $content_dir = WP_CONTENT_DIR; | |
| 114 | + $abspath = ABSPATH; | |
| 115 | + | |
| 116 | + if ( strpos( $lang_dir, $content_dir ) === 0 ) { | |
| 117 | + $data['baseUrl'] = content_url( | |
| 118 | + substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) | |
| 119 | + ); | |
| 120 | + } elseif ( strpos( $lang_dir, $abspath ) === 0 ) { | |
| 121 | + $data['baseUrl'] = site_url( | |
| 122 | + substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) | |
| 123 | + ); | |
| 124 | + } | |
| 125 | + | |
| 126 | + $locale_data = $this->get_preloaded_locale_data( $data['locale'] ); | |
| 127 | + $data['preloaded'] = ! empty( $locale_data ); | |
| 128 | + | |
| 129 | + // The consolidated locale data already covers every string in the bundles, | |
| 130 | + // so the per-chunk hash map is only needed when it could not be built. | |
| 131 | + if ( ! $data['preloaded'] ) { | |
| 132 | + $data['translationMap'] = (object) $this->get_translation_map(); | |
| 133 | + } | |
| 134 | + | |
| 135 | + wp_enqueue_script( 'booktics-i18n-loader' ); | |
| 136 | + | |
| 137 | + $data['domainMap'] = (object) $data['domainMap']; | |
| 138 | + $data['domainPaths'] = (object) $data['domainPaths']; | |
| 139 | + | |
| 140 | + $inline = '(function(){var k="bookticsI18nLoader"; if (typeof window[k] === "undefined") { window[k] = {}; } window[k].state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';'; | |
| 141 | + | |
| 142 | + if ( ! empty( $locale_data ) ) { | |
| 143 | + $inline .= ' window[k].preloadedLocaleData = window.bookticsPreloadedLocaleData = ' | |
| 144 | + . wp_json_encode( $locale_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . ';'; | |
| 145 | + } | |
| 146 | + | |
| 147 | + $inline .= '})();'; | |
| 148 | + | |
| 149 | + wp_add_inline_script( | |
| 150 | + 'booktics-i18n-loader', | |
| 151 | + $inline, | |
| 152 | + 'before' | |
| 153 | + ); | |
| 154 | + } | |
| 155 | + | |
| 156 | + /** | |
| 56 | 157 | * Register styles |
| 57 | 158 | * |
| 58 | - * @param array $styles | |
| 59 | - * | |
| 60 | 159 | * @return void |
| 61 | 160 | */ |
| 62 | 161 | public function register_styles() { |
| 63 | 162 | $styles = $this->get_styles(); |
| @@ -106,9 +205,9 @@ | ||
| 106 | 205 | return $assets; |
| 107 | 206 | } |
| 108 | 207 | |
| 109 | 208 | /** |
| 110 | - * Get script file version | |
| 209 | + * Get script file version from .asset.php (generated by DependencyExtractionWebpackPlugin). | |
| 111 | 210 | * |
| 112 | 211 | * @param string $file_name |
| 113 | 212 | * |
| 114 | 213 | * @return string |
| @@ -123,37 +222,193 @@ | ||
| 123 | 222 | * Get file path from url |
| 124 | 223 | * |
| 125 | 224 | * @param string $url |
| 126 | 225 | * |
| 127 | - * @return string | |
| 226 | + * @return string|false | |
| 128 | 227 | */ |
| 129 | 228 | private function get_file_path( $url ) { |
| 130 | - // Check if the URL is valid | |
| 131 | 229 | if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { |
| 132 | 230 | return false; |
| 133 | 231 | } |
| 134 | 232 | |
| 135 | - // Parse the URL | |
| 136 | 233 | $url_parts = wp_parse_url( $url ); |
| 137 | 234 | |
| 138 | - // Check if the URL has a path component | |
| 139 | 235 | if ( ! isset( $url_parts['path'] ) ) { |
| 140 | - return false; // URL does not contain a path | |
| 236 | + return false; | |
| 141 | 237 | } |
| 142 | 238 | |
| 143 | - $clean_path = str_replace( '.js', '.asset.php', $url_parts['path'] ); | |
| 239 | + $path = $url_parts['path']; | |
| 240 | + $path = preg_replace( '/\.js$/', '.asset.php', $path ); | |
| 144 | 241 | |
| 145 | - // Get the file path from the URL path. | |
| 146 | 242 | if ( isset( $_SERVER['DOCUMENT_ROOT'] ) ) { |
| 147 | - $file_path = sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $clean_path; | |
| 243 | + $file_path = sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $path; | |
| 148 | 244 | } else { |
| 149 | - $file_path = ''; // Or another appropriate fallback. | |
| 245 | + $file_path = ''; | |
| 150 | 246 | } |
| 151 | 247 | |
| 152 | - // Check if the file exists | |
| 153 | 248 | if ( ! file_exists( $file_path ) ) { |
| 154 | - return false; // File does not exist | |
| 249 | + return false; | |
| 155 | 250 | } |
| 156 | 251 | |
| 157 | 252 | return $file_path; |
| 253 | + } | |
| 254 | + | |
| 255 | + /** | |
| 256 | + * Build the Jed locale data for every translated string in the text domain. | |
| 257 | + * | |
| 258 | + * Script translations are normally split into one JSON file per bundled | |
| 259 | + * script, keyed by an md5 of that script's path. Async chunk filenames carry | |
| 260 | + * a content hash and change on every build, so those JSON files go stale as | |
| 261 | + * soon as the plugin is updated. Registering the whole domain up front keeps | |
| 262 | + * the React apps translated regardless of how the bundles were chunked. | |
| 263 | + * | |
| 264 | + * @param string $locale Locale being rendered. | |
| 265 | + * | |
| 266 | + * @return array Jed formatted locale data, empty when there is nothing to preload. | |
| 267 | + */ | |
| 268 | + private function get_preloaded_locale_data( $locale ) { | |
| 269 | + static $cache = []; | |
| 270 | + | |
| 271 | + if ( isset( $cache[ $locale ] ) ) { | |
| 272 | + return $cache[ $locale ]; | |
| 273 | + } | |
| 274 | + | |
| 275 | + $cache[ $locale ] = []; | |
| 276 | + | |
| 277 | + if ( 'en_US' === $locale ) { | |
| 278 | + return $cache[ $locale ]; | |
| 279 | + } | |
| 280 | + | |
| 281 | + if ( ! is_textdomain_loaded( 'booktics' ) ) { | |
| 282 | + // Registers the plugin's own /languages path. Just-in-time loading only | |
| 283 | + // finds WP.org language packs, so without this a bundled .mo is ignored. | |
| 284 | + // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound | |
| 285 | + load_plugin_textdomain( 'booktics', false, dirname( plugin_basename( BOOKTICS_FILE ) ) . '/languages' ); | |
| 286 | + } | |
| 287 | + | |
| 288 | + $translations = get_translations_for_domain( 'booktics' ); | |
| 289 | + | |
| 290 | + // WP_Translations (WP 6.5+, .l10n.php files) does not extend Translations, | |
| 291 | + // so check for the entries it exposes rather than the class. | |
| 292 | + if ( ! is_object( $translations ) || $translations instanceof \NOOP_Translations ) { | |
| 293 | + return $cache[ $locale ]; | |
| 294 | + } | |
| 295 | + | |
| 296 | + $entries = (array) $translations->entries; | |
| 297 | + $messages = []; | |
| 298 | + | |
| 299 | + foreach ( $entries as $entry ) { | |
| 300 | + if ( ! $entry instanceof \Translation_Entry || ! is_string( $entry->singular ) || '' === $entry->singular ) { | |
| 301 | + continue; | |
| 302 | + } | |
| 303 | + | |
| 304 | + $entry_translations = array_values( array_filter( | |
| 305 | + (array) $entry->translations, | |
| 306 | + function ( $translation ) { | |
| 307 | + return is_string( $translation ) && '' !== $translation; | |
| 308 | + } | |
| 309 | + ) ); | |
| 310 | + | |
| 311 | + if ( empty( $entry_translations ) ) { | |
| 312 | + continue; | |
| 313 | + } | |
| 314 | + | |
| 315 | + // wp.i18n keys contextual strings as "contextsingular". | |
| 316 | + $key = ( is_string( $entry->context ) && '' !== $entry->context ) | |
| 317 | + ? $entry->context . "\4" . $entry->singular | |
| 318 | + : $entry->singular; | |
| 319 | + | |
| 320 | + $messages[ $key ] = $entry_translations; | |
| 321 | + } | |
| 322 | + | |
| 323 | + if ( empty( $messages ) ) { | |
| 324 | + return $cache[ $locale ]; | |
| 325 | + } | |
| 326 | + | |
| 327 | + $headers = (array) $translations->headers; | |
| 328 | + | |
| 329 | + $locale_data = [ | |
| 330 | + '' => [ | |
| 331 | + 'domain' => 'booktics', | |
| 332 | + 'lang' => $locale, | |
| 333 | + 'plural-forms' => ! empty( $headers['Plural-Forms'] ) ? $headers['Plural-Forms'] : 'nplurals=2; plural=n != 1;', | |
| 334 | + ], | |
| 335 | + ] + $messages; | |
| 336 | + | |
| 337 | + /** | |
| 338 | + * Filter the locale data preloaded for the React apps. | |
| 339 | + * | |
| 340 | + * Useful to trim the payload down to the strings a site actually renders. | |
| 341 | + * | |
| 342 | + * @param array $locale_data Jed formatted locale data. | |
| 343 | + * @param string $locale Locale being rendered. | |
| 344 | + */ | |
| 345 | + $cache[ $locale ] = apply_filters( 'booktics_preloaded_locale_data', $locale_data, $locale ); | |
| 346 | + | |
| 347 | + return $cache[ $locale ]; | |
| 348 | + } | |
| 349 | + | |
| 350 | + /** | |
| 351 | + * Get map of translation file hashes to their URLs. | |
| 352 | + * | |
| 353 | + * @return array Hash => URL map. | |
| 354 | + */ | |
| 355 | + private function get_translation_map() { | |
| 356 | + $map = []; | |
| 357 | + $locale = determine_locale(); | |
| 358 | + $domain = 'booktics'; | |
| 359 | + | |
| 360 | + $locale_variants = array_unique( array_filter( [ | |
| 361 | + $locale, | |
| 362 | + str_replace( '-', '_', $locale ), | |
| 363 | + str_replace( '_', '-', $locale ), | |
| 364 | + ] ) ); | |
| 365 | + | |
| 366 | + foreach ( $this->get_translation_directories() as $dir ) { | |
| 367 | + if ( ! is_dir( $dir['path'] ) || ! is_readable( $dir['path'] ) ) { | |
| 368 | + continue; | |
| 369 | + } | |
| 370 | + | |
| 371 | + foreach ( $locale_variants as $variant ) { | |
| 372 | + $files = glob( $dir['path'] . $domain . '-' . $variant . '-*.json' ); | |
| 373 | + if ( ! $files ) { | |
| 374 | + continue; | |
| 375 | + } | |
| 376 | + | |
| 377 | + foreach ( $files as $file ) { | |
| 378 | + $filename = basename( $file ); | |
| 379 | + $variant_prefix = $domain . '-' . $variant . '-'; | |
| 380 | + if ( strpos( $filename, $variant_prefix ) === 0 ) { | |
| 381 | + $hash = substr( $filename, strlen( $variant_prefix ), -5 ); | |
| 382 | + if ( $hash ) { | |
| 383 | + $map[ $hash ] = trailingslashit( $dir['url'] ) . $filename; | |
| 384 | + } | |
| 385 | + } | |
| 386 | + } | |
| 387 | + } | |
| 388 | + } | |
| 389 | + | |
| 390 | + return $map; | |
| 391 | + } | |
| 392 | + | |
| 393 | + /** | |
| 394 | + * Return translation directory paths and their corresponding URLs. | |
| 395 | + * | |
| 396 | + * @return array<int, array{path: string, url: string}> | |
| 397 | + */ | |
| 398 | + private function get_translation_directories() { | |
| 399 | + return [ | |
| 400 | + [ | |
| 401 | + 'path' => WP_CONTENT_DIR . '/languages/plugins/', | |
| 402 | + 'url' => content_url( 'languages/plugins/' ), | |
| 403 | + ], | |
| 404 | + [ | |
| 405 | + 'path' => WP_CONTENT_DIR . '/languages/loco/plugins/', | |
| 406 | + 'url' => content_url( 'languages/loco/plugins/' ), | |
| 407 | + ], | |
| 408 | + [ | |
| 409 | + 'path' => booktics()->plugin_directory . '/languages/', | |
| 410 | + 'url' => booktics()->plugin_url . '/languages/', | |
| 411 | + ], | |
| 412 | + ]; | |
| 158 | 413 | } |
| 159 | 414 | } |