| @@ -105,8 +105,9 @@ | ||
| 105 | 105 | 'locale' => determine_locale(), |
| 106 | 106 | 'domainMap' => [], |
| 107 | 107 | 'domainPaths' => [], |
| 108 | 108 | 'translationMap' => (object) [], |
| 109 | + 'preloaded' => false, | |
| 109 | 110 | ]; |
| 110 | 111 | |
| 111 | 112 | $lang_dir = WP_LANG_DIR; |
| 112 | 113 | $content_dir = WP_CONTENT_DIR; |
| @@ -121,49 +122,27 @@ | ||
| 121 | 122 | substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) |
| 122 | 123 | ); |
| 123 | 124 | } |
| 124 | 125 | |
| 125 | - $data['translationMap'] = (object) $this->get_translation_map(); | |
| 126 | - $data['domainPaths'] = (object) $this->get_translation_directories(); | |
| 126 | + $locale_data = $this->get_preloaded_locale_data( $data['locale'] ); | |
| 127 | + $data['preloaded'] = ! empty( $locale_data ); | |
| 127 | 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 | + | |
| 128 | 135 | wp_enqueue_script( 'booktics-i18n-loader' ); |
| 129 | 136 | |
| 130 | 137 | $data['domainMap'] = (object) $data['domainMap']; |
| 131 | 138 | $data['domainPaths'] = (object) $data['domainPaths']; |
| 132 | 139 | |
| 133 | - $preload = null; | |
| 134 | - $l10n_file = trailingslashit( WP_LANG_DIR ) . 'plugins/booktics-' . $data['locale'] . '.l10n.php'; | |
| 135 | - $l10n_file_alt = trailingslashit( WP_LANG_DIR ) . 'plugins/booktics-' . str_replace( '-', '_', $data['locale'] ) . '.l10n.php'; | |
| 136 | - $l10n_file_legacy = trailingslashit( WP_LANG_DIR ) . 'plugins/booktics-' . str_replace( '_', '-', $data['locale'] ) . '.l10n.php'; | |
| 137 | - $l10n_candidates = array_unique( array_filter( [ $l10n_file, $l10n_file_alt, $l10n_file_legacy ] ) ); | |
| 138 | - | |
| 139 | - foreach ( $l10n_candidates as $candidate ) { | |
| 140 | - if ( file_exists( $candidate ) ) { | |
| 141 | - $preload = include $candidate; | |
| 142 | - break; | |
| 143 | - } | |
| 144 | - } | |
| 145 | - | |
| 146 | 140 | $inline = '(function(){var k="bookticsI18nLoader"; if (typeof window[k] === "undefined") { window[k] = {}; } window[k].state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';'; |
| 147 | 141 | |
| 148 | - if ( is_array( $preload ) && ! empty( $preload['messages'] ) && is_array( $preload['messages'] ) ) { | |
| 149 | - $messages = []; | |
| 150 | - foreach ( $preload['messages'] as $msgid => $msgstr ) { | |
| 151 | - if ( is_string( $msgid ) && is_string( $msgstr ) ) { | |
| 152 | - $messages[ $msgid ] = [ $msgstr ]; | |
| 153 | - } | |
| 154 | - } | |
| 155 | - | |
| 156 | - $locale_data = [ | |
| 157 | - '' => [ | |
| 158 | - 'domain' => 'booktics', | |
| 159 | - 'lang' => $data['locale'], | |
| 160 | - 'plural-forms' => isset( $preload['plural-forms'] ) ? $preload['plural-forms'] : 'nplurals=2; plural=n != 1;', | |
| 161 | - ], | |
| 162 | - ] + $messages; | |
| 163 | - | |
| 164 | - $encoded_locale_data = wp_json_encode( $locale_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); | |
| 165 | - $inline .= ' window[k].preloadedLocaleData = ' . $encoded_locale_data . '; window.bookticsPreloadedLocaleData = ' . $encoded_locale_data . ';'; | |
| 142 | + if ( ! empty( $locale_data ) ) { | |
| 143 | + $inline .= ' window[k].preloadedLocaleData = window.bookticsPreloadedLocaleData = ' | |
| 144 | + . wp_json_encode( $locale_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . ';'; | |
| 166 | 145 | } |
| 167 | 146 | |
| 168 | 147 | $inline .= '})();'; |
| 169 | 148 | |
| @@ -270,8 +249,103 @@ | ||
| 270 | 249 | return false; |
| 271 | 250 | } |
| 272 | 251 | |
| 273 | 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 ]; | |
| 274 | 348 | } |
| 275 | 349 | |
| 276 | 350 | /** |
| 277 | 351 | * Get map of translation file hashes to their URLs. |