PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.28
Booktics – Appointment Booking Calendar for Service Businesses v1.0.28
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
← All changes | core/assets/base-assets.php +117 -66 1.0.21 → 1.0.28 View file →
@@ -28,9 +28,8 @@
28 28 *
29 29 * @return void
30 30 */
31 31 public function register_styles_scripts() {
32 - // Register i18n loader early so other scripts can depend on it.
33 32 $this->register_global_scripts();
34 33 $this->register_scripts();
35 34 $this->register_styles();
36 35 }
@@ -37,10 +36,8 @@
37 36
38 37 /**
39 38 * Register scripts
40 39 *
41 - * @param array $scripts
42 - *
43 40 * @return void
44 41 */
45 42 public function register_scripts() {
46 43 $scripts = $this->get_scripts();
@@ -51,9 +48,8 @@
51 48 $version = isset( $script['version'] ) ? $script['version'] : $this->get_version( $script['src'] );
52 49
53 50 $deps = $this->get_dependencies( $script['src'], $deps );
54 51
55 - // Mirror wp-cafe: if script uses wp-i18n, ensure our i18n loader runs first.
56 52 if ( in_array( 'wp-i18n', $deps, true ) ) {
57 53 $deps[] = 'booktics-i18n-loader';
58 54 }
59 55
@@ -58,16 +54,12 @@
58 54 }
59 55
60 56 wp_register_script( $handle, $script['src'], $deps, $version, $in_footer );
61 57
62 - // Enable JS translations for bundles that use wp.i18n.
63 58 if ( function_exists( 'wp_set_script_translations' ) ) {
64 - // Don't force a path: translations are commonly in WP_LANG_DIR/plugins.
65 59 wp_set_script_translations( $handle, 'booktics' );
66 60 }
67 61
68 - // Ensure preloaded locale data is actually registered in wp.i18n for
69 - // pages that never trigger per-chunk JSON downloads.
70 62 if ( 'booktics-packages' === $handle ) {
71 63 wp_add_inline_script(
72 64 'booktics-packages',
73 65 '(function(){try{if(window.bookticsPreloadedLocaleData && window.wp && window.wp.i18n && window.wp.i18n.setLocaleData){window.wp.i18n.setLocaleData(window.bookticsPreloadedLocaleData,"booktics");}}catch(e){}})();',
@@ -113,8 +105,9 @@
113 105 'locale' => determine_locale(),
114 106 'domainMap' => [],
115 107 'domainPaths' => [],
116 108 'translationMap' => (object) [],
109 + 'preloaded' => false,
117 110 ];
118 111
119 112 $lang_dir = WP_LANG_DIR;
120 113 $content_dir = WP_CONTENT_DIR;
@@ -129,51 +122,27 @@
129 122 substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) )
130 123 );
131 124 }
132 125
133 - $data['translationMap'] = (object) $this->get_translation_map();
134 - $data['domainPaths'] = (object) $this->get_translation_directories();
126 + $locale_data = $this->get_preloaded_locale_data( $data['locale'] );
127 + $data['preloaded'] = ! empty( $locale_data );
135 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 +
136 135 wp_enqueue_script( 'booktics-i18n-loader' );
137 136
138 137 $data['domainMap'] = (object) $data['domainMap'];
139 138 $data['domainPaths'] = (object) $data['domainPaths'];
140 139
141 - // If a consolidated .l10n.php file exists, preload all messages to avoid
142 - // relying on per-chunk JSON requests (which may be missing).
143 - $preload = null;
144 - $l10n_file = trailingslashit( WP_LANG_DIR ) . 'plugins/booktics-' . $data['locale'] . '.l10n.php';
145 - $l10n_file_alt = trailingslashit( WP_LANG_DIR ) . 'plugins/booktics-' . str_replace( '-', '_', $data['locale'] ) . '.l10n.php';
146 - $l10n_file_legacy = trailingslashit( WP_LANG_DIR ) . 'plugins/booktics-' . str_replace( '_', '-', $data['locale'] ) . '.l10n.php';
147 - $l10n_candidates = array_unique( array_filter( [ $l10n_file, $l10n_file_alt, $l10n_file_legacy ] ) );
148 -
149 - foreach ( $l10n_candidates as $candidate ) {
150 - if ( file_exists( $candidate ) ) {
151 - $preload = include $candidate;
152 - break;
153 - }
154 - }
155 -
156 140 $inline = '(function(){var k="bookticsI18nLoader"; if (typeof window[k] === "undefined") { window[k] = {}; } window[k].state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) . ';';
157 141
158 - if ( is_array( $preload ) && ! empty( $preload['messages'] ) && is_array( $preload['messages'] ) ) {
159 - $messages = [];
160 - foreach ( $preload['messages'] as $msgid => $msgstr ) {
161 - if ( is_string( $msgid ) && is_string( $msgstr ) ) {
162 - $messages[ $msgid ] = [ $msgstr ];
163 - }
164 - }
165 -
166 - $locale_data = [
167 - '' => [
168 - 'domain' => 'booktics',
169 - 'lang' => $data['locale'],
170 - 'plural-forms' => isset( $preload['plural-forms'] ) ? $preload['plural-forms'] : 'nplurals=2; plural=n != 1;',
171 - ],
172 - ] + $messages;
173 -
174 - $encoded_locale_data = wp_json_encode( $locale_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
175 - $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 ) . ';';
176 145 }
177 146
178 147 $inline .= '})();';
179 148
@@ -186,10 +155,8 @@
186 155
187 156 /**
188 157 * Register styles
189 158 *
190 - * @param array $styles
191 - *
192 159 * @return void
193 160 */
194 161 public function register_styles() {
195 162 $styles = $this->get_styles();
@@ -238,9 +205,9 @@
238 205 return $assets;
239 206 }
240 207
241 208 /**
242 - * Get script file version
209 + * Get script file version from .asset.php (generated by DependencyExtractionWebpackPlugin).
243 210 *
244 211 * @param string $file_name
245 212 *
246 213 * @return string
@@ -255,36 +222,32 @@
255 222 * Get file path from url
256 223 *
257 224 * @param string $url
258 225 *
259 - * @return string
226 + * @return string|false
260 227 */
261 228 private function get_file_path( $url ) {
262 - // Check if the URL is valid
263 229 if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
264 230 return false;
265 231 }
266 232
267 - // Parse the URL
268 233 $url_parts = wp_parse_url( $url );
269 234
270 - // Check if the URL has a path component
271 235 if ( ! isset( $url_parts['path'] ) ) {
272 - return false; // URL does not contain a path
236 + return false;
273 237 }
274 238
275 - $clean_path = str_replace( '.js', '.asset.php', $url_parts['path'] );
239 + $path = $url_parts['path'];
240 + $path = preg_replace( '/\.js$/', '.asset.php', $path );
276 241
277 - // Get the file path from the URL path.
278 242 if ( isset( $_SERVER['DOCUMENT_ROOT'] ) ) {
279 - $file_path = sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $clean_path;
243 + $file_path = sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) . $path;
280 244 } else {
281 - $file_path = ''; // Or another appropriate fallback.
245 + $file_path = '';
282 246 }
283 247
284 - // Check if the file exists
285 248 if ( ! file_exists( $file_path ) ) {
286 - return false; // File does not exist.
249 + return false;
287 250 }
288 251
289 252 return $file_path;
290 253 }
@@ -289,13 +252,105 @@
289 252 return $file_path;
290 253 }
291 254
292 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 + /**
293 351 * Get map of translation file hashes to their URLs.
294 352 *
295 - * Scans all translation directories for JSON files matching
296 - * the current locale and domain, building a hash => URL map.
297 - *
298 353 * @return array Hash => URL map.
299 354 */
300 355 private function get_translation_map() {
301 356 $map = [];
@@ -319,13 +374,12 @@
319 374 continue;
320 375 }
321 376
322 377 foreach ( $files as $file ) {
323 - $filename = basename( $file );
324 - // Extract hash: everything after "{domain}-{locale}-".
378 + $filename = basename( $file );
325 379 $variant_prefix = $domain . '-' . $variant . '-';
326 380 if ( strpos( $filename, $variant_prefix ) === 0 ) {
327 - $hash = substr( $filename, strlen( $variant_prefix ), -5 ); // strip .json
381 + $hash = substr( $filename, strlen( $variant_prefix ), -5 );
328 382 if ( $hash ) {
329 383 $map[ $hash ] = trailingslashit( $dir['url'] ) . $filename;
330 384 }
331 385 }
@@ -337,11 +391,8 @@
337 391 }
338 392
339 393 /**
340 394 * Return translation directory paths and their corresponding URLs.
341 - *
342 - * Covers the standard WP languages/plugins/ directory, Loco Translate's
343 - * directory, and the plugin's own bundled languages/ folder.
344 395 *
345 396 * @return array<int, array{path: string, url: string}>
346 397 */
347 398 private function get_translation_directories() {