| @@ -228,13 +228,15 @@ | ||
| 228 | 228 | |
| 229 | 229 | // Register scripts and styles first |
| 230 | 230 | if ( $this->is_request( 'admin' ) ) { |
| 231 | 231 | add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] ); |
| 232 | + add_action( 'admin_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 ); | |
| 232 | 233 | add_action( 'admin_footer', [ $this, 'admin_helpscout_beacon' ] ); |
| 233 | 234 | } |
| 234 | 235 | |
| 235 | 236 | if ( $this->is_request( 'frontend' ) ) { |
| 236 | 237 | add_action( 'wp_enqueue_scripts', array( $this, 'frontend_scripts' ) ); |
| 238 | + add_action( 'wp_enqueue_scripts', [ $this, 'preload_js_translations' ], 5 ); | |
| 237 | 239 | } |
| 238 | 240 | |
| 239 | 241 | // Register admin menu |
| 240 | 242 | Core\Admin\Menu::instance()->init(); |
| @@ -261,8 +263,109 @@ | ||
| 261 | 263 | return "https://arraytics.com/timetics/"; |
| 262 | 264 | } |
| 263 | 265 | |
| 264 | 266 | /** |
| 267 | + * Preload PHP-loaded translations into wp.i18n before any bundle runs. | |
| 268 | + * | |
| 269 | + * Why: webpack bundles collapse many source files into one. wp_set_script_translations() | |
| 270 | + * looks up a JSON file hashed from the registered bundle URL, but Loco and wp i18n make-json | |
| 271 | + * shard JSON by source-file path. The hashes never match, so per-script JSON loading silently | |
| 272 | + * fails. Reading the textdomain that load_text_domain() already populated and injecting it via | |
| 273 | + * setLocaleData() bypasses the hash mismatch entirely — every __() call across every bundle | |
| 274 | + * resolves from in-memory data set before any React component renders. | |
| 275 | + */ | |
| 276 | + public function preload_js_translations() { | |
| 277 | + $locale = determine_locale(); | |
| 278 | + $plural_forms = 'nplurals=2; plural=(n != 1);'; | |
| 279 | + | |
| 280 | + // Read .mo first — canonical, always-fresh source that Loco rewrites on every save. | |
| 281 | + // .l10n.php cache often lags behind .mo because Loco does not regenerate it. | |
| 282 | + $messages = $this->extract_mo_messages( $locale, $plural_forms ); | |
| 283 | + | |
| 284 | + // Merge .l10n.php on top (covers WP 6.5+ system-translated plugins where only .l10n.php exists). | |
| 285 | + foreach ( [ | |
| 286 | + WP_LANG_DIR . '/loco/plugins/timetics-' . $locale . '.l10n.php', | |
| 287 | + TIMETICS_PLUGIN_DIR . 'languages/timetics-' . $locale . '.l10n.php', | |
| 288 | + WP_LANG_DIR . '/plugins/timetics-' . $locale . '.l10n.php', | |
| 289 | + ] as $file ) { | |
| 290 | + if ( ! is_readable( $file ) ) { | |
| 291 | + continue; | |
| 292 | + } | |
| 293 | + $data = include $file; | |
| 294 | + if ( ! is_array( $data ) || empty( $data['messages'] ) ) { | |
| 295 | + continue; | |
| 296 | + } | |
| 297 | + $messages = array_merge( $messages, $data['messages'] ); | |
| 298 | + if ( ! empty( $data['plural-forms'] ) ) { | |
| 299 | + $plural_forms = $data['plural-forms']; | |
| 300 | + } | |
| 301 | + } | |
| 302 | + | |
| 303 | + if ( empty( $messages ) ) { | |
| 304 | + return; | |
| 305 | + } | |
| 306 | + | |
| 307 | + $locale_data = [ | |
| 308 | + '' => [ | |
| 309 | + 'domain' => 'timetics', | |
| 310 | + 'lang' => $locale, | |
| 311 | + 'plural-forms' => $plural_forms, | |
| 312 | + ], | |
| 313 | + ]; | |
| 314 | + | |
| 315 | + foreach ( $messages as $msgid => $msgstr ) { | |
| 316 | + if ( is_string( $msgid ) && $msgid !== '' && is_string( $msgstr ) && $msgstr !== '' ) { | |
| 317 | + $locale_data[ $msgid ] = [ $msgstr ]; | |
| 318 | + } | |
| 319 | + } | |
| 320 | + | |
| 321 | + wp_add_inline_script( | |
| 322 | + 'wp-i18n', | |
| 323 | + sprintf( | |
| 324 | + 'wp.i18n.setLocaleData( %s, "timetics" );', | |
| 325 | + wp_json_encode( $locale_data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) | |
| 326 | + ), | |
| 327 | + 'after' | |
| 328 | + ); | |
| 329 | + } | |
| 330 | + | |
| 331 | + /** | |
| 332 | + * Fallback: parse .mo file to extract all translations. | |
| 333 | + */ | |
| 334 | + private function extract_mo_messages( $locale, &$plural_forms ) { | |
| 335 | + $candidates = [ | |
| 336 | + WP_LANG_DIR . '/loco/plugins/timetics-' . $locale . '.mo', | |
| 337 | + TIMETICS_PLUGIN_DIR . 'languages/timetics-' . $locale . '.mo', | |
| 338 | + WP_LANG_DIR . '/plugins/timetics-' . $locale . '.mo', | |
| 339 | + ]; | |
| 340 | + | |
| 341 | + $messages = []; | |
| 342 | + | |
| 343 | + foreach ( $candidates as $mofile ) { | |
| 344 | + if ( ! is_readable( $mofile ) ) { | |
| 345 | + continue; | |
| 346 | + } | |
| 347 | + if ( ! class_exists( 'MO' ) ) { | |
| 348 | + require_once ABSPATH . WPINC . '/pomo/mo.php'; | |
| 349 | + } | |
| 350 | + $mo = new \MO(); | |
| 351 | + if ( ! $mo->import_from_file( $mofile ) ) { | |
| 352 | + continue; | |
| 353 | + } | |
| 354 | + if ( ! empty( $mo->headers['Plural-Forms'] ) ) { | |
| 355 | + $plural_forms = $mo->headers['Plural-Forms']; | |
| 356 | + } | |
| 357 | + foreach ( $mo->entries as $msgid => $entry ) { | |
| 358 | + if ( ! empty( $entry->translations[0] ) ) { | |
| 359 | + $messages[ $msgid ] = $entry->translations[0]; | |
| 360 | + } | |
| 361 | + } | |
| 362 | + } | |
| 363 | + | |
| 364 | + return $messages; | |
| 365 | + } | |
| 366 | + | |
| 367 | + /** | |
| 265 | 368 | * Register scripts and styles for admin |
| 266 | 369 | * |
| 267 | 370 | * @return void |
| 268 | 371 | */ |
| @@ -302,8 +405,9 @@ | ||
| 302 | 405 | } |
| 303 | 406 | |
| 304 | 407 | wp_enqueue_script( 'timetics-dashboard-scripts', TIMETICS_ASSETS_URL . 'js/dashboard.js', [ 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data', 'wp-color-picker' ], TIMETICS_VERSION, true ); |
| 305 | 408 | |
| 409 | + wp_set_script_translations( 'timetics-dashboard-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' ); | |
| 306 | 410 | |
| 307 | 411 | $localize_obj = array( |
| 308 | 412 | 'site_url' => site_url(), |
| 309 | 413 | 'admin_url' => admin_url(), |
| @@ -321,8 +425,11 @@ | ||
| 321 | 425 | 'current_user_id' => get_current_user_id(), |
| 322 | 426 | 'timeticsPro' => class_exists('TimeticsPro') ? true : false, |
| 323 | 427 | 'demo_url' => $this->demo_url(), |
| 324 | 428 | 'current_user' => timetics_get_current_user(), |
| 429 | + 'aisentic' => $this->get_aisentic_state(), | |
| 430 | + 'theme_mode' => Core\Admin\Theme_Mode::get_for_localize(), | |
| 431 | + 'dark_mode_enabled' => Core\Admin\Theme_Mode::is_enabled(), | |
| 325 | 432 | ); |
| 326 | 433 | |
| 327 | 434 | $localize_obj = apply_filters( 'timetics_admin_localize_data', $localize_obj ); |
| 328 | 435 | |
| @@ -329,8 +436,37 @@ | ||
| 329 | 436 | wp_localize_script( 'timetics-dashboard-scripts', 'timetics', $localize_obj ); |
| 330 | 437 | } |
| 331 | 438 | |
| 332 | 439 | /** |
| 440 | + * Aisentic connect state for the onboarding consent box and dashboard banner. | |
| 441 | + * | |
| 442 | + * Both surfaces show the email before the user opts in, so the value comes | |
| 443 | + * from the same resolver the connect request uses. | |
| 444 | + * | |
| 445 | + * @return array Aisentic identity plus active/registered flags. | |
| 446 | + */ | |
| 447 | + private function get_aisentic_state() { | |
| 448 | + $identity = timetics_aisentic_identity(); | |
| 449 | + | |
| 450 | + return [ | |
| 451 | + 'name' => $identity['name'], | |
| 452 | + 'email' => $identity['email'], | |
| 453 | + 'active' => class_exists( 'Aisentic\Init' ), | |
| 454 | + 'registered' => timetics_aisentic_is_registered(), | |
| 455 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ), | |
| 456 | + /* | |
| 457 | + * Aisentic builds before the registration handshake have no | |
| 458 | + * listener for our hook. Connecting would fail silently there, so | |
| 459 | + * the UI hides the offer instead of promising something dead. | |
| 460 | + */ | |
| 461 | + 'supports_registration' => class_exists( 'Aisentic\Api\Services\Registration_Service' ), | |
| 462 | + 'terms_url' => 'https://arraytics.com/terms-of-service/', | |
| 463 | + // Only people who could install Aisentic by hand get the setup dialog. | |
| 464 | + 'can_setup' => current_user_can( 'manage_options' ) && current_user_can( 'install_plugins' ), | |
| 465 | + ]; | |
| 466 | + } | |
| 467 | + | |
| 468 | + /** | |
| 333 | 469 | * Register scripts and styles for frontend |
| 334 | 470 | * |
| 335 | 471 | * @return void |
| 336 | 472 | */ |
| @@ -361,10 +497,10 @@ | ||
| 361 | 497 | |
| 362 | 498 | wp_register_script( 'timetics-frontend-scripts', TIMETICS_ASSETS_URL . 'js/frontend.js', [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true ); |
| 363 | 499 | |
| 364 | 500 | $calendar_locale = timetics_get_option('calendar_locale'); |
| 365 | - if (!empty($calendar_locale)) { | |
| 366 | - wp_register_script( 'calendar-locale', "https://npmcdn.com/flatpickr/dist/l10n/{$calendar_locale}.js", [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true ); | |
| 501 | + if (!empty($calendar_locale) && file_exists( TIMETICS_PLUGIN_DIR . "assets/js/local/{$calendar_locale}.js" ) ) { | |
| 502 | + wp_register_script( 'calendar-locale', TIMETICS_ASSETS_URL . "js/local/{$calendar_locale}.js", [ 'jquery', 'wp-plugins', 'wp-i18n', 'wp-element', 'wp-dom', 'wp-data' ], TIMETICS_VERSION, true ); | |
| 367 | 503 | } |
| 368 | 504 | |
| 369 | 505 | // load text domain |
| 370 | 506 | wp_set_script_translations( 'timetics-frontend-scripts', 'timetics', TIMETICS_PLUGIN_DIR . 'languages/' ); |
| @@ -463,9 +599,9 @@ | ||
| 463 | 599 | |
| 464 | 600 | \Wpmet\Libs\Banner::instance('timetics') |
| 465 | 601 | ->is_test(true) |
| 466 | 602 | ->set_filter(ltrim($filter_string, ',')) |
| 467 | - ->set_api_url('https://banner.themefunction.com/public/jhanda') | |
| 603 | + ->set_api_url('https://banner.arraytics.com/public/jhanda') | |
| 468 | 604 | ->set_plugin_screens('timetics') |
| 469 | 605 | ->set_plugin_screens('toplevel_page_timetics') |
| 470 | 606 | ->call(); |
| 471 | 607 | // show get-help and upgrade-to-premium menu. |
| @@ -503,9 +639,9 @@ | ||
| 503 | 639 | color: "#0060e5", |
| 504 | 640 | }); |
| 505 | 641 | window.Beacon('init', '4be24aa2-ee50-483c-a90c-db4216664d65'); |
| 506 | 642 | window.Beacon('on', 'ready', function(){ |
| 507 | - window.Beacon('show'); | |
| 643 | + window.Beacon('close'); | |
| 508 | 644 | }); |
| 509 | 645 | |
| 510 | 646 | </script> |
| 511 | 647 | <?php |