PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | app/Providers/FrontendAssetsProvider.php +531 -11 3.0.3 → 3.0.16 View file →
@@ -30,8 +30,50 @@
30 30 add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
31 31 }
32 32
33 33 /**
34 + * Resolve the effective trip-listing card layout for one listing.
35 + *
36 + * The site-wide Design setting (`frontend_listing_card_layout`) is the
37 + * default; a shortcode/block may pass a per-instance override. Anything that
38 + * is not a concrete layout (empty, "inherit", or an unknown value) falls
39 + * back to the global setting, so existing shortcodes/blocks are unchanged.
40 + *
41 + * @param string $override Per-instance override (card_layout / cardLayout).
42 + * @return string One of: standard | compact_mobile | compact_all.
43 + */
44 + public static function resolveListingLayout(string $override = ''): string
45 + {
46 + $override = strtolower(trim($override));
47 + if (in_array($override, ['standard', 'compact_mobile', 'compact_all'], true)) {
48 + return $override;
49 + }
50 +
51 + $global = \Yatra\Services\SettingsService::getString('frontend_listing_card_layout', 'standard');
52 + return in_array($global, ['standard', 'compact_mobile', 'compact_all'], true) ? $global : 'standard';
53 + }
54 +
55 + /**
56 + * Map a resolved layout to the container CSS class(es) the listing CSS keys
57 + * off. Placed on the listing container (archive .yatra-listing-page, or the
58 + * shortcode/block wrapper) so each listing can carry its own layout and an
59 + * override never leaks into other listings on the same page.
60 + *
61 + * @param string $layout standard | compact_mobile | compact_all
62 + * @return string Space-separated class list ('' for standard).
63 + */
64 + public static function listingLayoutClasses(string $layout): string
65 + {
66 + if ($layout === 'compact_mobile') {
67 + return 'yatra-listing-compact';
68 + }
69 + if ($layout === 'compact_all') {
70 + return 'yatra-listing-compact yatra-listing-compact--all';
71 + }
72 + return '';
73 + }
74 +
75 + /**
34 76 * Register Font Awesome (optional) and common.css so block editor + shortcode styles can
35 77 * depend on `yatra-common` (shared @keyframes: yatra-spin, yatra-shimmer, etc.).
36 78 */
37 79 /**
@@ -232,9 +274,21 @@
232 274 $dependencies = ['jquery'];
233 275 if ($handle === 'trip') {
234 276 $dependencies[] = 'yatra-api-helper';
235 277 }
236 -
278 + // Scripts that call `wp.i18n.__()` for user-facing
279 + // strings need wp-i18n as a dependency so the global
280 + // exists before they run AND a `wp_set_script_translations`
281 + // call below so WordPress loads each one's Jed JSON
282 + // catalog (each handle has its own md5-named JSON
283 + // because the .po references their respective source
284 + // file paths). Add a handle here whenever you wrap a
285 + // new string in __() inside its file.
286 + $i18nHandles = ['trip', 'listing', 'stripe', 'tour-viewer', 'video-player'];
287 + if (in_array($handle, $i18nHandles, true)) {
288 + $dependencies[] = 'wp-i18n';
289 + }
290 +
237 291 wp_enqueue_script(
238 292 "yatra-{$handle}",
239 293 YATRA_PLUGIN_URL . "assets/js/{$filename}",
240 294 $dependencies,
@@ -240,8 +294,22 @@
240 294 $dependencies,
241 295 YATRA_VERSION . '.' . filemtime($filePath),
242 296 true
243 297 );
298 +
299 + // Mirror the wp-i18n dep list above. wp_set_script_translations
300 + // tells WordPress where to look for this script's Jed JSON
301 + // catalog (path = plugin's i18n/languages/) and the loader
302 + // hashes md5(handle src) to find the right file.
303 + if (in_array($handle, $i18nHandles, true)
304 + && function_exists('wp_set_script_translations')
305 + ) {
306 + wp_set_script_translations(
307 + "yatra-{$handle}",
308 + 'yatra',
309 + YATRA_PLUGIN_PATH . 'i18n/languages'
310 + );
311 + }
244 312 }
245 313 }
246 314
247 315 if (\Yatra\Services\SettingsService::wishlistEnabled()) {
@@ -249,17 +317,46 @@
249 317 if (file_exists($wishPath)) {
250 318 wp_enqueue_script(
251 319 'yatra-listing-wishlist',
252 320 YATRA_PLUGIN_URL . 'assets/js/listing-wishlist.js',
253 - ['jquery'],
321 + ['jquery', 'wp-i18n'],
254 322 YATRA_VERSION . '.' . filemtime($wishPath),
255 323 true
256 324 );
325 + if (function_exists('wp_set_script_translations')) {
326 + wp_set_script_translations(
327 + 'yatra-listing-wishlist',
328 + 'yatra',
329 + YATRA_PLUGIN_PATH . 'i18n/languages'
330 + );
331 + }
332 + // Wishlist "Login" should send guests to the configured
333 + // My Account page (Settings → Permalink slug), not the raw
334 + // wp-login.php screen. Fall back to wp_login_url() only when
335 + // no account base is configured so the button never dead-ends.
336 + $yatraAccountBase = \Yatra\Services\SettingsService::getAccountBase();
337 + $yatraAccountLoginUrl = $yatraAccountBase !== ''
338 + ? home_url('/' . trim($yatraAccountBase, '/') . '/')
339 + : wp_login_url();
257 340 wp_localize_script('yatra-listing-wishlist', 'yatraWishlistConfig', [
258 341 'enabled' => true,
259 342 'restUrl' => rest_url('yatra/v1'),
260 343 'nonce' => wp_create_nonce('wp_rest'),
261 344 'isLoggedIn' => is_user_logged_in(),
345 + 'loginUrl' => $yatraAccountLoginUrl,
346 + 'i18n' => [
347 + 'loginRequired' => __('Login Required', 'yatra'),
348 + 'loginPrompt' => __('Please login to save trips to your wishlist.', 'yatra'),
349 + 'login' => __('Login', 'yatra'),
350 + 'cancel' => __('Cancel', 'yatra'),
351 + 'genericError' => __('An error occurred. Please try again.', 'yatra'),
352 + 'saved' => __('Trip saved to wishlist', 'yatra'),
353 + 'removed' => __('Trip removed from wishlist', 'yatra'),
354 + 'saveFailed' => __('Failed to save trip', 'yatra'),
355 + 'removeFailed' => __('Failed to remove trip', 'yatra'),
356 + 'addAria' => __('Add to favorites', 'yatra'),
357 + 'removeAria' => __('Remove from favorites', 'yatra'),
358 + ],
262 359 ]);
263 360 }
264 361 }
265 362 }
@@ -326,13 +423,49 @@
326 423 if (file_exists($bookingJs)) {
327 424 wp_enqueue_script(
328 425 'yatra-booking',
329 426 YATRA_PLUGIN_URL . 'assets/js/booking.js',
330 - ['jquery'],
427 + ['jquery', 'wp-i18n'],
331 428 YATRA_VERSION . '.' . filemtime($bookingJs),
332 429 true
333 430 );
431 + if (function_exists('wp_set_script_translations')) {
432 + wp_set_script_translations(
433 + 'yatra-booking',
434 + 'yatra',
435 + YATRA_PLUGIN_PATH . 'i18n/languages'
436 + );
437 + }
334 438 }
439 +
440 + // International phone-number widget (country flag + dial code) used by
441 + // the booking form's tel fields.
442 + $this->enqueuePhoneInputAssets();
443 + $this->enqueueCountrySelectAssets();
444 +
445 + // Mobile sticky-sidebar + flatpickr init for the single-trip page. Lives in a
446 + // dedicated file rather than as inline <script> in the partial because
447 + // WordPress core's `convert_chars` filter (hooked to the_content) rewrites the
448 + // `&&` operators inside inline scripts as `&#038;&#038;` — JS parsers don't
449 + // decode HTML entities inside <script>, producing a SyntaxError. As a properly
450 + // enqueued external file, the source is delivered verbatim.
451 + $sidebarJs = YATRA_PLUGIN_PATH . 'assets/js/single-trip-sidebar.js';
452 + if (file_exists($sidebarJs)) {
453 + wp_enqueue_script(
454 + 'yatra-single-trip-sidebar',
455 + YATRA_PLUGIN_URL . 'assets/js/single-trip-sidebar.js',
456 + ['yatra-trip', 'wp-i18n'], // depends on window.yatraTripData from yatra-trip
457 + YATRA_VERSION . '.' . filemtime($sidebarJs),
458 + true
459 + );
460 + if (function_exists('wp_set_script_translations')) {
461 + wp_set_script_translations(
462 + 'yatra-single-trip-sidebar',
463 + 'yatra',
464 + YATRA_PLUGIN_PATH . 'i18n/languages'
465 + );
466 + }
467 + }
335 468
336 469 // Localize trip page data for JS (trip.js, booking.js)
337 470 global $trip;
338 471
@@ -359,8 +492,16 @@
359 492 'tripId' => $trip_id,
360 493 'tripSlug' => $trip_slug,
361 494 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
362 495 'isLoggedIn' => is_user_logged_in(),
496 + // Wishlist "Login" (guest) → the configured My Account page
497 + // (Settings → Permalink slug), NOT wp-login.php. Without this key
498 + // trip.js falls back to a hardcoded '/wp-login.php'. Falls back to
499 + // wp_login_url() only when no account base slug is configured.
500 + 'loginUrl' => (function () {
501 + $base = \Yatra\Services\SettingsService::getAccountBase();
502 + return $base !== '' ? home_url('/' . trim($base, '/') . '/') : wp_login_url();
503 + })(),
363 504 // Regional settings
364 505 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
365 506 'dateFormat' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
366 507 'timeFormat' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
@@ -367,9 +508,9 @@
367 508 // Currency/settings
368 509 'currency' => \Yatra\Services\SettingsService::getCurrency(),
369 510 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
370 511 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
371 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
512 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
372 513 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
373 514 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
374 515 'basePrice' => 0.0,
375 516 'currencySymbol' => function_exists('yatra_get_currency_symbol')
@@ -384,8 +525,9 @@
384 525 ]),
385 526 'pricingType' => 'regular',
386 527 'sidebarAvailability' => [],
387 528 'sidebarGroupDiscounts' => [],
529 + 'flatpickrLocale' => $this->buildFlatpickrLocalePayload(),
388 530 ];
389 531
390 532 if ($has_trip) {
391 533 if (function_exists('yatra_single_trip_calculate_base_price')) {
@@ -426,13 +568,119 @@
426 568 'isRemainingPayment' => false,
427 569 'remainingAmount' => 0,
428 570 'totalAmount' => 0,
429 571 'amountPaid' => 0,
572 + // Booking-scoped CSRF nonce — covers BOTH logged-in and
573 + // guest checkouts. The REST endpoint's public
574 + // permission_callback intentionally bypasses the WP REST
575 + // cookie/nonce check (so guests can hit it at all);
576 + // this token is what gates the actual write. The JS
577 + // forwards it in the `X-Yatra-Booking-Nonce` header on
578 + // every booking-create / booking-update POST.
579 + 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
430 580 ]
431 581 ));
432 582 }
433 583
434 584 /**
585 + * Enqueue the country selector widget (assets/js/country-select.js +
586 + * assets/css/country-select.css).
587 + *
588 + * Upgrades every `type => country` field (Country, Nationality, on both the
589 + * contact and traveler sections) into a searchable dropdown showing the
590 + * national flag, matching the phone country-code control. Purely additive:
591 + * the underlying <select> still renders and submits, so a site that never
592 + * loads this script behaves exactly as before.
593 + *
594 + * Idempotent, so it is safe to call from every path that renders the form.
595 + *
596 + * @return void
597 + */
598 + private function enqueueCountrySelectAssets(): void
599 + {
600 + if (wp_script_is('yatra-country-select', 'enqueued')) {
601 + return;
602 + }
603 +
604 + $css = YATRA_PLUGIN_PATH . 'assets/css/country-select.css';
605 + if (file_exists($css)) {
606 + wp_enqueue_style(
607 + 'yatra-country-select',
608 + YATRA_PLUGIN_URL . 'assets/css/country-select.css',
609 + [],
610 + YATRA_VERSION . '.' . filemtime($css)
611 + );
612 + }
613 +
614 + $js = YATRA_PLUGIN_PATH . 'assets/js/country-select.js';
615 + if (!file_exists($js)) {
616 + return;
617 + }
618 +
619 + wp_enqueue_script(
620 + 'yatra-country-select',
621 + YATRA_PLUGIN_URL . 'assets/js/country-select.js',
622 + [],
623 + YATRA_VERSION . '.' . filemtime($js),
624 + true
625 + );
626 +
627 + wp_localize_script('yatra-country-select', 'yatraCountrySelectData', [
628 + 'i18n' => [
629 + 'search' => __('Search country', 'yatra'),
630 + 'noResults' => __('No matches', 'yatra'),
631 + ],
632 + ]);
633 + }
634 +
635 + /**
636 + * Enqueue the international phone-number widget (assets/js/phone-input.js +
637 + * assets/css/phone-input.css) and localize its country + dial-code dataset.
638 + *
639 + * Self-contained (reads its own `yatraPhoneData` global) and idempotent, so
640 + * it can be called from every path that renders the booking form. Country
641 + * data is the single source of truth in {@see FormatHelper}.
642 + *
643 + * @return void
644 + */
645 + private function enqueuePhoneInputAssets(): void
646 + {
647 + if (wp_script_is('yatra-phone-input', 'enqueued')) {
648 + return;
649 + }
650 +
651 + $css = YATRA_PLUGIN_PATH . 'assets/css/phone-input.css';
652 + if (file_exists($css)) {
653 + wp_enqueue_style(
654 + 'yatra-phone-input',
655 + YATRA_PLUGIN_URL . 'assets/css/phone-input.css',
656 + [],
657 + YATRA_VERSION . '.' . filemtime($css)
658 + );
659 + }
660 +
661 + $js = YATRA_PLUGIN_PATH . 'assets/js/phone-input.js';
662 + if (!file_exists($js)) {
663 + return;
664 + }
665 + wp_enqueue_script(
666 + 'yatra-phone-input',
667 + YATRA_PLUGIN_URL . 'assets/js/phone-input.js',
668 + [],
669 + YATRA_VERSION . '.' . filemtime($js),
670 + true
671 + );
672 + wp_localize_script('yatra-phone-input', 'yatraPhoneData', [
673 + 'countries' => \Yatra\Helpers\FormatHelper::getPhoneCountries(),
674 + 'priority' => \Yatra\Helpers\FormatHelper::getPhonePriority(),
675 + 'i18n' => [
676 + 'search' => __('Search country', 'yatra'),
677 + 'noResults' => __('No matches', 'yatra'),
678 + ],
679 + ]);
680 + }
681 +
682 + /**
435 683 * Enqueue activity listing specific assets
436 684 *
437 685 * @return void
438 686 */
@@ -486,20 +734,71 @@
486 734 YATRA_VERSION . '.' . filemtime($bookingCss)
487 735 );
488 736 }
489 737
738 + // Flatpickr — used by booking.js to upgrade Date-of-Birth (and other
739 + // date) inputs to a picker with fast, typeable year navigation. The
740 + // single-trip page already ships flatpickr (see single-trip.php); the
741 + // dedicated booking page did not, so enqueue it here. booking.js
742 + // self-guards on `typeof flatpickr`, so this is safe either way.
743 + wp_enqueue_style(
744 + 'yatra-flatpickr',
745 + 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css',
746 + [],
747 + YATRA_VERSION
748 + );
749 + wp_enqueue_script(
750 + 'yatra-flatpickr',
751 + 'https://cdn.jsdelivr.net/npm/flatpickr',
752 + [],
753 + YATRA_VERSION,
754 + true
755 + );
756 +
490 757 // Enqueue booking-specific JavaScript
491 758 $bookingJs = YATRA_PLUGIN_PATH . 'assets/js/booking.js';
492 759 if (file_exists($bookingJs)) {
760 + // booking.js renders user-facing strings via wp.i18n.__() (the
761 + // "Processing..." button label and the per-gateway info messages
762 + // shown when a payment method is selected). It therefore needs
763 + // wp-i18n as a dependency AND wp_set_script_translations so its
764 + // Jed catalog loads — mirroring the trip-detail enqueue above.
765 + // Without these, those strings stay English on the standalone
766 + // booking page regardless of site locale.
493 767 wp_enqueue_script(
494 768 'yatra-booking',
495 769 YATRA_PLUGIN_URL . 'assets/js/booking.js',
496 - ['jquery'],
770 + ['jquery', 'yatra-flatpickr', 'wp-i18n'],
497 771 YATRA_VERSION . '.' . filemtime($bookingJs),
498 772 true
499 773 );
774 + if (function_exists('wp_set_script_translations')) {
775 + wp_set_script_translations(
776 + 'yatra-booking',
777 + 'yatra',
778 + YATRA_PLUGIN_PATH . 'i18n/languages'
779 + );
780 + }
500 781 }
501 -
782 +
783 + // International phone-number widget (country flag + dial code).
784 + $this->enqueuePhoneInputAssets();
785 + $this->enqueueCountrySelectAssets();
786 +
787 + // Load each available gateway's own client scripts on the checkout page
788 + // (e.g. Square Web Payments SDK + square.js, Authorize.Net Accept.js +
789 + // its handler, Razorpay SDK + its handler). Every gateway's
790 + // enqueueScripts() self-guards on isAvailable(), so only enabled +
791 + // configured gateways load anything. This call was previously missing,
792 + // so Pro gateways that render an inline card form shipped no JS to
793 + // checkout and clicking "Pay" just span the button forever. It is
794 + // additive and safe for the others: Stripe's enqueueScripts() is a
795 + // no-op (Stripe is loaded via enqueueCommonJs), and PayPal/Pay Later
796 + // have no client scripts.
797 + if (class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
798 + \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance()->enqueueScripts();
799 + }
800 +
502 801 // Localize booking data for booking.js
503 802 $permalink_structure = get_option('permalink_structure') ?: '';
504 803 $is_plain = empty($permalink_structure);
505 804
@@ -530,16 +829,21 @@
530 829 'siteUrl' => site_url(),
531 830 'bookingBase' => \Yatra\Services\SettingsService::getBookingBase(),
532 831 'permalinkStructure' => $is_plain ? 'plain' : $permalink_structure,
533 832 'nonce' => wp_create_nonce('wp_rest'),
833 + // Booking-scoped CSRF nonce. See enqueueTripDetailAssets()
834 + // for the rationale: the booking REST endpoint bypasses
835 + // the WP REST cookie/nonce check (so guests can use it),
836 + // and this token is what gates the actual booking write.
837 + 'bookingNonce' => wp_create_nonce('yatra_booking_action'),
534 838 'currency' => \Yatra\Services\SettingsService::getCurrency(),
535 839 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
536 840 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
537 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
841 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
538 842 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
539 843 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
540 844 // Payment gateways data
541 - 'paymentGateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])),
845 + 'paymentGateways' => $this->sanitizeGatewayConfigsForFrontend(apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', []))),
542 846 'paymentMethods' => \Yatra\Services\SettingsService::get('payment_methods', []),
543 847 'paymentTestMode' => \Yatra\Services\SettingsService::get('payment_test_mode', false),
544 848 'partialPayment' => \Yatra\Services\SettingsService::get('partial_payment', false),
545 849 'partialPaymentPercentage' => \Yatra\Services\SettingsService::get('partial_payment_percentage', 0),
@@ -551,10 +855,17 @@
551 855 'gatewayOrder' => \Yatra\Services\SettingsService::get('gateway_order', []),
552 856 'autoConfirmPayLater' => \Yatra\Services\SettingsService::get('auto_confirm_pay_later', true),
553 857 'allowWaitlist' => \Yatra\Services\SettingsService::isEnabled('allow_waitlist'),
554 858 'waitlistAutoConfirm' => \Yatra\Services\SettingsService::isEnabled('waitlist_auto_confirm'),
555 - 'gateways' => apply_filters('yatra_payment_gateways', \Yatra\Services\SettingsService::get('payment_gateways', [])),
556 - 'enabledGateways' => \Yatra\Services\SettingsService::get('payment_gateways', []),
859 + 'gateways' => $this->getGatewayFrontendConfigs(),
860 + 'enabledGateways' => $this->sanitizeGatewayConfigsForFrontend(\Yatra\Services\SettingsService::get('payment_gateways', [])),
861 + // Server-side translated UI strings for booking.js. PHP __() resolves via .mo
862 + // (reliable), so these stay translatable even when the JS-translation JSON
863 + // chain (wp_set_script_translations) doesn't load on a given setup.
864 + 'i18n' => [
865 + 'complete_booking' => __('Complete Booking', 'yatra'),
866 + 'pay_now' => __('Pay Now', 'yatra'),
867 + ],
557 868 ];
558 869
559 870 $bookingData = array_merge($bookingData, $this->getStripeFrontendBookingPayload());
560 871
@@ -617,15 +928,34 @@
617 928 'companyEmail' => \Yatra\Services\SettingsService::getString('company_email', ''),
618 929 'currency' => \Yatra\Services\SettingsService::getCurrency(),
619 930 'currencyPosition' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
620 931 'currency_position' => \Yatra\Services\SettingsService::getString('currency_position', 'left'),
621 - 'decimalPlaces' => (int) \Yatra\Services\SettingsService::getString('currency_decimals', '2'),
932 + 'decimalPlaces' => \Yatra\Services\SettingsService::getPriceDecimals(),
622 933 'thousandSeparator' => \Yatra\Services\SettingsService::getString('thousand_separator', ','),
623 934 'decimalSeparator' => \Yatra\Services\SettingsService::getString('decimal_separator', '.'),
624 935 'locale' => get_locale(),
936 + // Global date/time format so the customer account pages render dates
937 + // in the operator's configured format (Settings → General), not a
938 + // hardcoded browser style. Keys mirror what the admin app receives.
939 + 'date_format' => \Yatra\Services\SettingsService::getString('date_format', 'Y-m-d'),
940 + 'time_format' => \Yatra\Services\SettingsService::getString('time_format', 'H:i'),
941 + 'timezone' => \Yatra\Services\SettingsService::getString('timezone', 'UTC'),
942 + // Full ISO country map (code => name) so the account profile can show
943 + // full country names and render the country dropdown. Mirrors the
944 + // admin (`yatraAdmin.countries`); honours the `yatra_countries_list` filter.
945 + 'countries' => class_exists('\\Yatra\\Helpers\\FormatHelper')
946 + ? \Yatra\Helpers\FormatHelper::getCountries()
947 + : [],
625 948 'translations' => $this->getFrontendTranslations(),
626 949 'wishlistEnabled' => \Yatra\Services\SettingsService::wishlistEnabled(),
627 950 ]);
951 +
952 + // Match admin React (`yatra-admin`): register Jed translations for this handle so `wp.i18n` resolves
953 + // strings from PHP/Loco JSON catalogs. Without this, only keys in `translations` above work; the rest
954 + // stay English because the account bundle is not in `yatra-admin`'s Jed file (different script hash).
955 + if (function_exists('wp_set_script_translations')) {
956 + wp_set_script_translations('yatra-account-page', 'yatra', YATRA_PLUGIN_PATH . 'i18n/languages');
957 + }
628 958 }
629 959
630 960 /**
631 961 * Enqueue listing filters JavaScript
@@ -656,8 +986,80 @@
656 986 }
657 987 }
658 988
659 989 /**
990 + * Month and weekday labels for Flatpickr from {@see \WP_Locale} (site language).
991 + *
992 + * @return array<string, mixed>
993 + */
994 + private function buildFlatpickrLocalePayload(): array
995 + {
996 + global $wp_locale;
997 +
998 + $first_day = (int) get_option('start_of_week', 1);
999 + $first_day = max(0, min(6, $first_day));
1000 +
1001 + if (!($wp_locale instanceof \WP_Locale)) {
1002 + return [
1003 + 'firstDayOfWeek' => $first_day,
1004 + ];
1005 + }
1006 +
1007 + // IMPORTANT — `month_abbrev` and `weekday_abbrev` are keyed by the
1008 + // TRANSLATED LONG NAME, not by a numeric index:
1009 + //
1010 + // $wp_locale->month['01'] = 'January' (or 'जनवरी', 'enero'…)
1011 + // $wp_locale->month_abbrev['January'] = 'Jan' (or 'जन', 'ene'…)
1012 + //
1013 + // An earlier version of this code mistakenly indexed
1014 + // month_abbrev by '01'..'12' / weekday_abbrev by 0..6, which
1015 + // ALWAYS returned null → flatpickr's locale.months.shorthand
1016 + // shipped as an array of empty strings → the `M` token in any
1017 + // altFormat rendered as nothing. Net effect: a date set to
1018 + // "19 May 2026" displayed as "19 2026" (no month) under any
1019 + // non-en_US locale that exposed the bug.
1020 + //
1021 + // WP_Locale exposes get_month_abbrev() / get_weekday_abbrev()
1022 + // which take the long name and do the right lookup. We use
1023 + // those so the indexing rule lives inside core, not here.
1024 + $months_long = [];
1025 + $months_short = [];
1026 + for ($m = 1; $m <= 12; ++$m) {
1027 + $key = sprintf('%02d', $m);
1028 + $long = $wp_locale->month[$key] ?? '';
1029 + $short = $long !== '' ? (string) $wp_locale->get_month_abbrev($long) : '';
1030 + $months_long[] = $long;
1031 + // Final fallback to the long name if the locale has no
1032 + // abbreviated form — better than shipping an empty string
1033 + // that flatpickr would render as blank.
1034 + $months_short[] = $short !== '' ? $short : $long;
1035 + }
1036 +
1037 + $weekdays_long = [];
1038 + $weekdays_short = [];
1039 + for ($d = 0; $d <= 6; ++$d) {
1040 + $long = $wp_locale->weekday[$d] ?? '';
1041 + $short = $long !== '' ? (string) $wp_locale->get_weekday_abbrev($long) : '';
1042 + $weekdays_long[] = $long;
1043 + $weekdays_short[] = $short !== '' ? $short : $long;
1044 + }
1045 +
1046 + $payload = [
1047 + 'weekdays' => [
1048 + 'shorthand' => $weekdays_short,
1049 + 'longhand' => $weekdays_long,
1050 + ],
1051 + 'months' => [
1052 + 'shorthand' => $months_short,
1053 + 'longhand' => $months_long,
1054 + ],
1055 + 'firstDayOfWeek' => $first_day,
1056 + ];
1057 +
1058 + return apply_filters('yatra_flatpickr_locale', $payload);
1059 + }
1060 +
1061 + /**
660 1062 * Get frontend translations
661 1063 *
662 1064 * @return array
663 1065 */
@@ -678,8 +1080,34 @@
678 1080 'Total Amount' => __('Total Amount', 'yatra'),
679 1081 'Payment Status' => __('Payment Status', 'yatra'),
680 1082 'View Details' => __('View Details', 'yatra'),
681 1083
1084 + // Traveler / contact / emergency field labels on the account page.
1085 + // Keep in sync with the `fieldLabel()` map in account/BookingDetails.tsx.
1086 + 'First Name' => __('First Name', 'yatra'),
1087 + 'Last Name' => __('Last Name', 'yatra'),
1088 + 'Full Name' => __('Full Name', 'yatra'),
1089 + 'Name' => __('Name', 'yatra'),
1090 + 'Email' => __('Email', 'yatra'),
1091 + 'Phone' => __('Phone', 'yatra'),
1092 + 'Mobile' => __('Mobile', 'yatra'),
1093 + 'Date of Birth' => __('Date of Birth', 'yatra'),
1094 + 'Gender' => __('Gender', 'yatra'),
1095 + 'Nationality' => __('Nationality', 'yatra'),
1096 + 'Country' => __('Country', 'yatra'),
1097 + 'Address' => __('Address', 'yatra'),
1098 + 'City' => __('City', 'yatra'),
1099 + 'State' => __('State', 'yatra'),
1100 + 'Postal Code' => __('Postal Code', 'yatra'),
1101 + 'Zip Code' => __('Zip Code', 'yatra'),
1102 + 'Passport' => __('Passport', 'yatra'),
1103 + 'Passport Number' => __('Passport Number', 'yatra'),
1104 + 'Passport Expiry' => __('Passport Expiry', 'yatra'),
1105 + 'Dietary Requirements' => __('Dietary Requirements', 'yatra'),
1106 + 'Special Requirements' => __('Special Requirements', 'yatra'),
1107 + 'Relationship' => __('Relationship', 'yatra'),
1108 + 'Company' => __('Company', 'yatra'),
1109 +
682 1110 // Common
683 1111 'Loading...' => __('Loading...', 'yatra'),
684 1112 'No data available' => __('No data available', 'yatra'),
685 1113 'Error loading data' => __('Error loading data', 'yatra'),
@@ -784,8 +1212,100 @@
784 1212 {
785 1213 $basePath = $type === 'css' ? 'assets/css/' : 'assets/js/';
786 1214 $fullPath = YATRA_PLUGIN_PATH . $basePath . $path;
787 1215 return file_exists($fullPath);
1216 + }
1217 +
1218 + /**
1219 + * Strip secret credentials from per-gateway config before it is localized
1220 + * into the page (yatraBookingData). The stored payment_gateways option
1221 + * holds private keys / access tokens that must NEVER reach the browser; the
1222 + * checkout scripts only ever read public values (publishable keys, Square
1223 + * application/location IDs, Authorize.Net public client key, the enabled
1224 + * flag, etc.). This removes the known secret keys while preserving the
1225 + * structure and every public field, so existing gateways/consumers are
1226 + * unaffected — only secrets are dropped.
1227 + *
1228 + * @param mixed $gateways
1229 + * @return array<string, mixed>
1230 + */
1231 + /**
1232 + * Per-gateway PUBLIC config for the booking page, keyed by gateway id
1233 + * (window.yatraBookingData.gateways.<id>). Checkout scripts read their public
1234 + * settings from here — e.g. square.js → gateways.square.application_id /
1235 + * location_id, authorizenet.js → gateways.authorize_net.public_client_key /
1236 + * api_login_id.
1237 + *
1238 + * Source of truth is each ENABLED gateway's own getFrontendData(), i.e. an
1239 + * allowlist the gateway itself declares. This is deliberately NOT a denylist
1240 + * over the raw stored config: a denylist would leak any secret whose key we
1241 + * forgot (e.g. Stripe live_secret_key / test_secret_key, Bank Transfer
1242 + * account_number / routing_code). Gateways without a getFrontendData()
1243 + * (Bank Transfer, PayPal, Pay Later, …) contribute nothing, so their stored
1244 + * details never reach the browser. Disabled gateways are excluded.
1245 + *
1246 + * @return array<string, array<string, mixed>>
1247 + */
1248 + private function getGatewayFrontendConfigs(): array
1249 + {
1250 + if (!class_exists(\Yatra\PaymentGateways\PaymentGatewayRegistry::class)) {
1251 + return [];
1252 + }
1253 +
1254 + $out = [];
1255 + try {
1256 + $registry = \Yatra\PaymentGateways\PaymentGatewayRegistry::getInstance();
1257 + foreach ($registry->getEnabledGateways() as $id => $gateway) {
1258 + if (!is_object($gateway) || !method_exists($gateway, 'getFrontendData')) {
1259 + continue;
1260 + }
1261 + $data = $gateway->getFrontendData();
1262 + if (is_array($data) && $data !== []) {
1263 + $data['enabled'] = true;
1264 + $out[(string) $id] = $data;
1265 + }
1266 + }
1267 + } catch (\Throwable $e) {
1268 + return [];
1269 + }
1270 +
1271 + return $out;
1272 + }
1273 +
1274 + private function sanitizeGatewayConfigsForFrontend($gateways): array
1275 + {
1276 + if (!is_array($gateways)) {
1277 + return [];
1278 + }
1279 +
1280 + // Credential fields that are private to the server.
1281 + $secretKeys = [
1282 + 'access_token',
1283 + 'api_key',
1284 + 'api_secret',
1285 + 'secret_key',
1286 + 'key_secret',
1287 + 'client_secret',
1288 + 'transaction_key',
1289 + 'webhook_secret',
1290 + 'webhook_signing_secret',
1291 + 'signing_secret',
1292 + 'private_key',
1293 + 'password',
1294 + 'secret',
1295 + ];
1296 +
1297 + $clean = [];
1298 + foreach ($gateways as $id => $config) {
1299 + if (is_array($config)) {
1300 + foreach ($secretKeys as $secret) {
1301 + unset($config[$secret]);
1302 + }
1303 + }
1304 + $clean[$id] = $config;
1305 + }
1306 +
1307 + return $clean;
788 1308 }
789 1309
790 1310 /**
791 1311 * Stripe Elements (assets/js/stripe.js) expects publishableKey under yatraBookingData.stripe.