| @@ -3,8 +3,48 @@ | ||
| 3 | 3 | * Class-based implementation for trip page features |
| 4 | 4 | */ |
| 5 | 5 | /* jshint esversion: 6 */ |
| 6 | 6 | |
| 7 | +// Translation helpers (booking.js installs the same shims if it runs first; | |
| 8 | +// idempotent setup so either load order works). | |
| 9 | +// Parse a numeric min/max attribute while preserving a legitimate 0. | |
| 10 | +// `parseInt(el.getAttribute('max')) || 99` treated max="0" — what a sold-out | |
| 11 | +// departure renders — as "unset" and fell back to the placeholder ceiling, so | |
| 12 | +// the guest selector stayed usable and ignored the departure's real capacity. | |
| 13 | +// Only an absent or non-numeric value may fall back. Installed once and shared | |
| 14 | +// by every frontend script, so load order does not matter. | |
| 15 | +if (typeof window.yatraNumOr !== 'function') { | |
| 16 | + window.yatraNumOr = function (raw, fallback) { | |
| 17 | + var parsed = parseInt(raw, 10); | |
| 18 | + return isNaN(parsed) ? fallback : parsed; | |
| 19 | + }; | |
| 20 | +} | |
| 21 | + | |
| 22 | +(function () { | |
| 23 | + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null; | |
| 24 | + if (i18n && typeof i18n.__ === 'function') { | |
| 25 | + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); }; | |
| 26 | + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); }; | |
| 27 | + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); }; | |
| 28 | + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s). | |
| 29 | + // Do NOT defer to a pre-existing window.sprintf: a theme or other | |
| 30 | + // plugin can define an incompatible global that leaves placeholders | |
| 31 | + // unsubstituted (the cause of literal "%d"/"%s" in the UI). | |
| 32 | + if (typeof i18n.sprintf === 'function') { | |
| 33 | + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); }; | |
| 34 | + } else if (typeof window.sprintf !== 'function') { | |
| 35 | + window.sprintf = function (fmt) { return fmt; }; | |
| 36 | + } | |
| 37 | + } else { | |
| 38 | + // wp.i18n not present — install passthroughs only if nothing usable | |
| 39 | + // exists yet (don't clobber a real binding installed earlier). | |
| 40 | + if (typeof window.__ !== 'function') window.__ = function (text) { return text; }; | |
| 41 | + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; }; | |
| 42 | + if (typeof window._x !== 'function') window._x = function (text) { return text; }; | |
| 43 | + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; }; | |
| 44 | + } | |
| 45 | +})(); | |
| 46 | + | |
| 7 | 47 | (function() { |
| 8 | 48 | 'use strict'; |
| 9 | 49 | |
| 10 | 50 | /** |
| @@ -64,8 +104,25 @@ | ||
| 64 | 104 | const php = cfg.dateFormat || cfg.date_format || 'Y-m-d'; |
| 65 | 105 | return phpDateFormatToFlatpickr(php); |
| 66 | 106 | }; |
| 67 | 107 | |
| 108 | + /** | |
| 109 | + * Flatpickr locale from PHP ({@see FrontendAssetsProvider::buildFlatpickrLocalePayload}). | |
| 110 | + * @returns {Record<string, unknown>} | |
| 111 | + */ | |
| 112 | + const getFlatpickrLocale = () => { | |
| 113 | + const cfg = window.yatraTripData || {}; | |
| 114 | + const l = cfg.flatpickrLocale; | |
| 115 | + if (!l || typeof l !== 'object') { | |
| 116 | + return { firstDayOfWeek: 1 }; | |
| 117 | + } | |
| 118 | + const out = Object.assign({}, l); | |
| 119 | + if (typeof out.firstDayOfWeek !== 'number' || Number.isNaN(out.firstDayOfWeek)) { | |
| 120 | + out.firstDayOfWeek = 1; | |
| 121 | + } | |
| 122 | + return out; | |
| 123 | + }; | |
| 124 | + | |
| 68 | 125 | const getRestBase = () => { |
| 69 | 126 | const siteUrl = |
| 70 | 127 | (window.yatraTripData && window.yatraTripData.siteUrl) || |
| 71 | 128 | (window.yatraAdmin && window.yatraAdmin.siteUrl) || |
| @@ -148,9 +205,14 @@ | ||
| 148 | 205 | const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.'; |
| 149 | 206 | |
| 150 | 207 | const num = Number(amount) || 0; |
| 151 | 208 | |
| 152 | - const formatted = new Intl.NumberFormat(undefined, { | |
| 209 | + // Format with a FIXED 'en-US' base (grouping ',', decimal '.') so the | |
| 210 | + // replace-with-configured-separators below is deterministic. With the | |
| 211 | + // viewer's locale (undefined), a European browser already yields | |
| 212 | + // '1.234,56' and the naive comma/dot replacement SWAPS the separators — | |
| 213 | + // making the configured decimal separator appear ignored. | |
| 214 | + const formatted = new Intl.NumberFormat('en-US', { | |
| 153 | 215 | minimumFractionDigits: decimals, |
| 154 | 216 | maximumFractionDigits: decimals, |
| 155 | 217 | }).format(num) |
| 156 | 218 | .replace(/,/g, 'TEMP_THOUSAND') |
| @@ -276,9 +338,17 @@ | ||
| 276 | 338 | const mediaData = JSON.parse(mediaScript.textContent || '{}'); |
| 277 | 339 | const imageUrls = Array.isArray(mediaData.images) ? mediaData.images : []; |
| 278 | 340 | imageUrls.forEach((url, index) => { |
| 279 | 341 | if (url && typeof url === 'string' && !this.images.some(i => i.src === url)) { |
| 280 | - this.images.push({ src: url, alt: `Gallery Image ${index + 1}` }); | |
| 342 | + this.images.push({ | |
| 343 | + src: url, | |
| 344 | + // Indexed alt text — sprintf isn't guaranteed | |
| 345 | + // available on every page, so we replace() the | |
| 346 | + // %d placeholder ourselves. Keeps the msgid a | |
| 347 | + // literal so makepot extracts cleanly. | |
| 348 | + /* translators: %d is the 1-based image index in the gallery. */ | |
| 349 | + alt: __('Gallery Image %d', 'yatra').replace('%d', String(index + 1)), | |
| 350 | + }); | |
| 281 | 351 | } |
| 282 | 352 | }); |
| 283 | 353 | } catch (e) { |
| 284 | 354 | // JSON parse error – fall through to DOM-based collection. |
| @@ -286,23 +356,24 @@ | ||
| 286 | 356 | } |
| 287 | 357 | |
| 288 | 358 | // 2. Collect from hero slides (covers any images not in the JSON blob). |
| 289 | 359 | document.querySelectorAll('.yatra-trip-hero-slide img').forEach((img, index) => { |
| 290 | - add(img, `Gallery Image ${index + 1}`); | |
| 360 | + /* translators: %d is the 1-based image index in the gallery. */ | |
| 361 | + add(img, __('Gallery Image %d', 'yatra').replace('%d', String(index + 1))); | |
| 291 | 362 | }); |
| 292 | 363 | |
| 293 | 364 | // 3. Collect from gallery section (hero gallery only, NOT itinerary). |
| 294 | 365 | document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item img').forEach((img) => { |
| 295 | - add(img, 'Gallery Image'); | |
| 366 | + add(img, __('Gallery Image', 'yatra')); | |
| 296 | 367 | }); |
| 297 | 368 | |
| 298 | 369 | // 4. Collect from hero main image. |
| 299 | 370 | const heroMainImg = document.querySelector('.yatra-hero-main-img'); |
| 300 | - if (heroMainImg) add(heroMainImg, 'Main Image'); | |
| 371 | + if (heroMainImg) add(heroMainImg, __('Main Image', 'yatra')); | |
| 301 | 372 | |
| 302 | 373 | // 5. Collect from side images. |
| 303 | 374 | document.querySelectorAll('.yatra-side-image-item img').forEach((img) => { |
| 304 | - add(img, 'Side Image'); | |
| 375 | + add(img, __('Side Image', 'yatra')); | |
| 305 | 376 | }); |
| 306 | 377 | } |
| 307 | 378 | |
| 308 | 379 | createThumbnails() { |
| @@ -496,14 +567,21 @@ | ||
| 496 | 567 | } |
| 497 | 568 | |
| 498 | 569 | /** |
| 499 | 570 | * Itinerary Gallery Modal Class |
| 500 | - * Separate gallery modal for itinerary section - independent from hero gallery | |
| 571 | + * Separate gallery modal for itinerary section - independent from hero gallery. | |
| 572 | + * | |
| 573 | + * Scoping: a single modal element is reused, but the image set is RECOMPUTED on | |
| 574 | + * every click from the *closest* `.yatra-entry-gallery` ancestor. Clicking a | |
| 575 | + * thumbnail in Day 2 must open only Day 2's images (starting at the clicked one) — | |
| 576 | + * never the global pool of every day's images. The hidden | |
| 577 | + * `.yatra-itinerary-trip-gallery` div on the page is for SEO / structured data | |
| 578 | + * only; it intentionally no longer drives the modal. | |
| 501 | 579 | */ |
| 502 | 580 | class ItineraryGalleryModal { |
| 503 | 581 | constructor() { |
| 504 | 582 | this.modal = null; |
| 505 | - this.images = []; | |
| 583 | + this.images = []; // currently-visible image set (the active scope) | |
| 506 | 584 | this.currentIndex = 0; |
| 507 | 585 | this.init(); |
| 508 | 586 | } |
| 509 | 587 | |
| @@ -520,43 +598,69 @@ | ||
| 520 | 598 | if (!this.modal) { |
| 521 | 599 | console.warn('Itinerary gallery modal not found'); |
| 522 | 600 | return; |
| 523 | 601 | } |
| 524 | - | |
| 525 | - console.log('Itinerary gallery modal found, collecting images...'); | |
| 526 | - this.collectImages(); | |
| 527 | - console.log('Itinerary gallery images collected:', this.images.length); | |
| 528 | - this.createThumbnails(); | |
| 529 | 602 | this.attachEventListeners(); |
| 530 | 603 | } |
| 531 | 604 | |
| 532 | - collectImages() { | |
| 533 | - this.images = []; | |
| 605 | + /** | |
| 606 | + * Build the image list from a scope element (an `.yatra-entry-gallery` block). | |
| 607 | + * Falls back to the document if no scope is given (preserves the legacy | |
| 608 | + * "global pool" behaviour for the SEO-only thumbnails clicker). | |
| 609 | + */ | |
| 610 | + collectImagesFrom(scope) { | |
| 611 | + const root = scope || document; | |
| 612 | + const images = []; | |
| 534 | 613 | |
| 535 | 614 | const add = (img, fallbackAlt) => { |
| 536 | 615 | const src = img.getAttribute('data-full-size') || getImageSrc(img); |
| 537 | 616 | if (!src) return; |
| 538 | - if (!this.images.some(i => i.src === src)) { | |
| 539 | - this.images.push({ src, alt: img.alt || fallbackAlt }); | |
| 617 | + if (!images.some(i => i.src === src)) { | |
| 618 | + images.push({ src, alt: img.alt || fallbackAlt }); | |
| 540 | 619 | } |
| 541 | 620 | }; |
| 542 | 621 | |
| 543 | - // Collect from itinerary gallery section (hidden gallery) | |
| 544 | - document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => { | |
| 545 | - add(img, `Itinerary Image ${this.images.length + 1}`); | |
| 622 | + // Visible gallery thumbnails inside the scope | |
| 623 | + root.querySelectorAll('.yatra-media-card img').forEach((img) => { | |
| 624 | + add(img, sprintf( | |
| 625 | + /* translators: %d: gallery image sequence number. */ | |
| 626 | + __('Itinerary Gallery Image %d', 'yatra'), | |
| 627 | + images.length + 1 | |
| 628 | + )); | |
| 546 | 629 | }); |
| 547 | 630 | |
| 548 | - // Collect from visible itinerary gallery items | |
| 549 | - document.querySelectorAll('.yatra-entry-gallery .yatra-media-card img').forEach((img) => { | |
| 550 | - add(img, `Itinerary Gallery Image ${this.images.length + 1}`); | |
| 631 | + // Video links inside the scope (fallback selector) | |
| 632 | + root.querySelectorAll('.yatra-itinerary-video-link img').forEach((img) => { | |
| 633 | + add(img, sprintf( | |
| 634 | + /* translators: %d: itinerary video sequence number. */ | |
| 635 | + __('Itinerary Video %d', 'yatra'), | |
| 636 | + images.length + 1 | |
| 637 | + )); | |
| 551 | 638 | }); |
| 552 | 639 | |
| 553 | - // Collect from itinerary video items | |
| 554 | - document.querySelectorAll('.yatra-entry-video .yatra-media-card img, .yatra-entry-gallery .yatra-itinerary-video-link img').forEach((img) => { | |
| 555 | - add(img, `Itinerary Video Image ${this.images.length + 1}`); | |
| 556 | - }); | |
| 640 | + // If still empty AND we were given no scope, fall back to the hidden global pool | |
| 641 | + // so legacy click handlers on `.yatra-itinerary-trip-gallery .yatra-gallery-item` | |
| 642 | + // continue to function. | |
| 643 | + if (images.length === 0 && !scope) { | |
| 644 | + document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => { | |
| 645 | + add(img, sprintf( | |
| 646 | + /* translators: %d: itinerary image sequence number. */ | |
| 647 | + __('Itinerary Image %d', 'yatra'), | |
| 648 | + images.length + 1 | |
| 649 | + )); | |
| 650 | + }); | |
| 651 | + } | |
| 652 | + | |
| 653 | + return images; | |
| 557 | 654 | } |
| 558 | 655 | |
| 656 | + /** Replace the active image set and rebuild thumbnails. */ | |
| 657 | + setActiveImages(images) { | |
| 658 | + this.images = images; | |
| 659 | + this.currentIndex = 0; | |
| 660 | + this.createThumbnails(); | |
| 661 | + } | |
| 662 | + | |
| 559 | 663 | createThumbnails() { |
| 560 | 664 | const container = this.modal.querySelector('.yatra-gallery-thumbnails-container'); |
| 561 | 665 | if (!container) return; |
| 562 | 666 | |
| @@ -677,30 +781,57 @@ | ||
| 677 | 781 | if (e.key === 'ArrowLeft') this.prev(); |
| 678 | 782 | if (e.key === 'ArrowRight') this.next(); |
| 679 | 783 | }); |
| 680 | 784 | |
| 681 | - // Attach click handlers to itinerary gallery links | |
| 682 | - const galleryLinks = document.querySelectorAll('.yatra-itinerary-gallery-link'); | |
| 683 | - console.log('Attaching listeners to itinerary gallery links:', galleryLinks.length); | |
| 684 | - galleryLinks.forEach((link) => { | |
| 685 | - link.addEventListener('click', (e) => { | |
| 686 | - e.preventDefault(); | |
| 687 | - const imageIndex = parseInt(link.getAttribute('data-image-index') || '0'); | |
| 688 | - console.log('Itinerary gallery link clicked, opening at index:', imageIndex); | |
| 689 | - this.open(imageIndex); | |
| 690 | - }); | |
| 691 | - }); | |
| 785 | + // Itinerary day gallery thumbnails — SCOPED to the clicked day. | |
| 786 | + // Every click recomputes the image set from the closest .yatra-entry-gallery so | |
| 787 | + // Day 2's modal only ever shows Day 2's photos, starting at the clicked one. | |
| 788 | + // Video links are intentionally NOT captured here — they have their own handler | |
| 789 | + // that opens YatraVideoPlayer (see further down in this file). | |
| 790 | + const onItineraryClick = (e) => { | |
| 791 | + const link = e.target.closest('.yatra-itinerary-gallery-link'); | |
| 792 | + if (!link) return; | |
| 793 | + // Don't hijack video link clicks even if they happen to match | |
| 794 | + if (link.classList.contains('yatra-itinerary-video-link')) return; | |
| 795 | + const scope = link.closest('.yatra-entry-gallery'); | |
| 796 | + if (!scope) return; | |
| 692 | 797 | |
| 693 | - // Attach click handlers to itinerary gallery items in hidden section | |
| 798 | + e.preventDefault(); | |
| 799 | + | |
| 800 | + const scopedImages = this.collectImagesFrom(scope); | |
| 801 | + if (scopedImages.length === 0) return; | |
| 802 | + | |
| 803 | + // Find the clicked image's index within the scoped set | |
| 804 | + const clickedImg = link.querySelector('img'); | |
| 805 | + const clickedSrc = clickedImg | |
| 806 | + ? (clickedImg.getAttribute('data-full-size') || getImageSrc(clickedImg)) | |
| 807 | + : null; | |
| 808 | + const startIndex = clickedSrc | |
| 809 | + ? Math.max(0, scopedImages.findIndex((i) => i.src === clickedSrc)) | |
| 810 | + : 0; | |
| 811 | + | |
| 812 | + this.setActiveImages(scopedImages); | |
| 813 | + this.open(startIndex); | |
| 814 | + }; | |
| 815 | + // Delegated listener — works even if entries are added/removed dynamically | |
| 816 | + document.addEventListener('click', onItineraryClick); | |
| 817 | + | |
| 818 | + // Legacy: clicks on the hidden SEO pool (kept for back-compat). These open | |
| 819 | + // the full pool because there's no per-day scope on the SEO container. | |
| 694 | 820 | const galleryItems = document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item'); |
| 695 | 821 | galleryItems.forEach((item, index) => { |
| 696 | 822 | item.addEventListener('click', () => { |
| 823 | + const allImages = this.collectImagesFrom(null); // fall back to global | |
| 824 | + if (allImages.length === 0) return; | |
| 697 | 825 | const img = item.querySelector('img'); |
| 698 | - if (img) { | |
| 699 | - const src = img.getAttribute('data-full-size') || getImageSrc(img); | |
| 700 | - const imageIndex = this.images.findIndex(i => i.src === src); | |
| 701 | - this.open(imageIndex >= 0 ? imageIndex : index); | |
| 702 | - } | |
| 826 | + const src = img | |
| 827 | + ? (img.getAttribute('data-full-size') || getImageSrc(img)) | |
| 828 | + : null; | |
| 829 | + const startIndex = src | |
| 830 | + ? Math.max(0, allImages.findIndex((i) => i.src === src)) | |
| 831 | + : index; | |
| 832 | + this.setActiveImages(allImages); | |
| 833 | + this.open(startIndex); | |
| 703 | 834 | }); |
| 704 | 835 | }); |
| 705 | 836 | } |
| 706 | 837 | } |
| @@ -855,9 +986,9 @@ | ||
| 855 | 986 | } |
| 856 | 987 | |
| 857 | 988 | if (images.length > 0) { |
| 858 | 989 | img.src = images[0]; |
| 859 | - img.alt = 'Trip Image 1'; | |
| 990 | + img.alt = __('Trip Image 1', 'yatra'); | |
| 860 | 991 | if (link) { |
| 861 | 992 | link.href = '#'; |
| 862 | 993 | link.dataset.imageIndex = '0'; |
| 863 | 994 | } |
| @@ -1075,8 +1206,12 @@ | ||
| 1075 | 1206 | if (existingOverlay) existingOverlay.remove(); |
| 1076 | 1207 | |
| 1077 | 1208 | const overlay = document.createElement('div'); |
| 1078 | 1209 | overlay.className = 'yatra-media-overlay yatra-media-tour-overlay'; |
| 1210 | + const tourFallbackTitle = __('360° Virtual Tour', 'yatra'); | |
| 1211 | + /* translators: %s: tour type (e.g. "360°"). */ | |
| 1212 | + const tourTypeLabel = sprintf(__('%s Experience', 'yatra'), tour.tour_type || '360°'); | |
| 1213 | + const tourActionLabel = __('Click to explore', 'yatra'); | |
| 1079 | 1214 | overlay.innerHTML = ` |
| 1080 | 1215 | <div class="yatra-tour-info"> |
| 1081 | 1216 | <div class="yatra-tour-icon"> |
| 1082 | 1217 | <svg width="80" height="80" viewBox="0 0 100 100" fill="none"> |
| @@ -1089,11 +1224,11 @@ | ||
| 1089 | 1224 | <text x="50" y="58" text-anchor="middle" fill="white" font-size="24" font-weight="bold">360°</text> |
| 1090 | 1225 | </svg> |
| 1091 | 1226 | </div> |
| 1092 | 1227 | <div class="yatra-tour-details"> |
| 1093 | - <div class="yatra-tour-title">${tour.title || '360° Virtual Tour'}</div> | |
| 1094 | - <div class="yatra-tour-type">${tour.tour_type || '360°'} Experience</div> | |
| 1095 | - <div class="yatra-tour-action">Click to explore</div> | |
| 1228 | + <div class="yatra-tour-title">${tour.title || tourFallbackTitle}</div> | |
| 1229 | + <div class="yatra-tour-type">${tourTypeLabel}</div> | |
| 1230 | + <div class="yatra-tour-action">${tourActionLabel}</div> | |
| 1096 | 1231 | </div> |
| 1097 | 1232 | </div> |
| 1098 | 1233 | `; |
| 1099 | 1234 | |
| @@ -1131,8 +1266,9 @@ | ||
| 1131 | 1266 | if (existingOverlay) existingOverlay.remove(); |
| 1132 | 1267 | |
| 1133 | 1268 | const overlay = document.createElement('div'); |
| 1134 | 1269 | overlay.className = 'yatra-media-overlay yatra-media-document-overlay'; |
| 1270 | + const documentFallbackTitle = __('Document', 'yatra'); | |
| 1135 | 1271 | overlay.innerHTML = ` |
| 1136 | 1272 | <div class="yatra-document-info"> |
| 1137 | 1273 | <div class="yatra-document-icon"> |
| 1138 | 1274 | <svg width="48" height="48" viewBox="0 0 24 24" fill="none"> |
| @@ -1142,9 +1278,9 @@ | ||
| 1142 | 1278 | <path d="M16 17H8" stroke="#374151" stroke-width="2"/> |
| 1143 | 1279 | </svg> |
| 1144 | 1280 | </div> |
| 1145 | 1281 | <div class="yatra-document-details"> |
| 1146 | - <div class="yatra-document-title">${doc.title || 'Document'}</div> | |
| 1282 | + <div class="yatra-document-title">${doc.title || documentFallbackTitle}</div> | |
| 1147 | 1283 | <div class="yatra-document-size">${this.formatFileSize(doc.file_size || 0)}</div> |
| 1148 | 1284 | </div> |
| 1149 | 1285 | </div> |
| 1150 | 1286 | `; |
| @@ -1151,11 +1287,11 @@ | ||
| 1151 | 1287 | container.appendChild(overlay); |
| 1152 | 1288 | } |
| 1153 | 1289 | |
| 1154 | 1290 | formatFileSize(bytes) { |
| 1155 | - if (bytes === 0) return '0 Bytes'; | |
| 1291 | + if (bytes === 0) return '0 ' + __('Bytes', 'yatra'); | |
| 1156 | 1292 | const k = 1024; |
| 1157 | - const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| 1293 | + const sizes = [__('Bytes', 'yatra'), 'KB', 'MB', 'GB']; | |
| 1158 | 1294 | const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 1159 | 1295 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| 1160 | 1296 | } |
| 1161 | 1297 | |
| @@ -1185,9 +1321,10 @@ | ||
| 1185 | 1321 | sideImages.forEach((item, index) => { |
| 1186 | 1322 | const img = item.querySelector('img'); |
| 1187 | 1323 | if (img && images[index + 1]) { |
| 1188 | 1324 | img.src = images[index + 1]; |
| 1189 | - img.alt = `Trip Image ${index + 2}`; | |
| 1325 | + /* translators: %d: image sequence number in the trip gallery. */ | |
| 1326 | + img.alt = sprintf(__('Trip Image %d', 'yatra'), index + 2); | |
| 1190 | 1327 | } |
| 1191 | 1328 | }); |
| 1192 | 1329 | } |
| 1193 | 1330 | |
| @@ -1192,8 +1329,9 @@ | ||
| 1192 | 1329 | } |
| 1193 | 1330 | |
| 1194 | 1331 | updateSideVideos(sideImages) { |
| 1195 | 1332 | const videos = this.mediaData.videos || []; |
| 1333 | + const fallbackVideoTitle = __('Trip Video', 'yatra'); | |
| 1196 | 1334 | sideImages.forEach((item, index) => { |
| 1197 | 1335 | const img = item.querySelector('img'); |
| 1198 | 1336 | if (img && videos[index + 1]) { |
| 1199 | 1337 | const video = videos[index + 1]; |
| @@ -1199,9 +1337,9 @@ | ||
| 1199 | 1337 | const video = videos[index + 1]; |
| 1200 | 1338 | if (video.thumbnail) { |
| 1201 | 1339 | img.src = video.thumbnail; |
| 1202 | 1340 | } |
| 1203 | - img.alt = video.title || 'Trip Video'; | |
| 1341 | + img.alt = video.title || fallbackVideoTitle; | |
| 1204 | 1342 | } |
| 1205 | 1343 | }); |
| 1206 | 1344 | } |
| 1207 | 1345 | |
| @@ -1206,8 +1344,9 @@ | ||
| 1206 | 1344 | } |
| 1207 | 1345 | |
| 1208 | 1346 | updateSideYoutubeVideos(sideImages) { |
| 1209 | 1347 | const youtubeVideos = this.mediaData.youtube_videos || []; |
| 1348 | + const fallbackYoutubeTitle = __('YouTube Video', 'yatra'); | |
| 1210 | 1349 | sideImages.forEach((item, index) => { |
| 1211 | 1350 | const img = item.querySelector('img'); |
| 1212 | 1351 | if (img && youtubeVideos[index + 1]) { |
| 1213 | 1352 | const video = youtubeVideos[index + 1]; |
| @@ -1213,9 +1352,9 @@ | ||
| 1213 | 1352 | const video = youtubeVideos[index + 1]; |
| 1214 | 1353 | if (video.thumbnail) { |
| 1215 | 1354 | img.src = video.thumbnail; |
| 1216 | 1355 | } |
| 1217 | - img.alt = video.title || 'YouTube Video'; | |
| 1356 | + img.alt = video.title || fallbackYoutubeTitle; | |
| 1218 | 1357 | } |
| 1219 | 1358 | }); |
| 1220 | 1359 | } |
| 1221 | 1360 | |
| @@ -1220,8 +1359,9 @@ | ||
| 1220 | 1359 | } |
| 1221 | 1360 | |
| 1222 | 1361 | updateSideTours(sideImages) { |
| 1223 | 1362 | const virtualTours = this.mediaData.virtual_tours || []; |
| 1363 | + const fallbackTourTitle = __('360° Virtual Tour', 'yatra'); | |
| 1224 | 1364 | sideImages.forEach((item, index) => { |
| 1225 | 1365 | const img = item.querySelector('img'); |
| 1226 | 1366 | if (img && virtualTours[index + 1]) { |
| 1227 | 1367 | const tour = virtualTours[index + 1]; |
| @@ -1227,9 +1367,9 @@ | ||
| 1227 | 1367 | const tour = virtualTours[index + 1]; |
| 1228 | 1368 | if (tour.thumbnail) { |
| 1229 | 1369 | img.src = tour.thumbnail; |
| 1230 | 1370 | } |
| 1231 | - img.alt = tour.title || '360° Virtual Tour'; | |
| 1371 | + img.alt = tour.title || fallbackTourTitle; | |
| 1232 | 1372 | } |
| 1233 | 1373 | }); |
| 1234 | 1374 | } |
| 1235 | 1375 | |
| @@ -1234,14 +1374,15 @@ | ||
| 1234 | 1374 | } |
| 1235 | 1375 | |
| 1236 | 1376 | updateSideDocuments(sideImages) { |
| 1237 | 1377 | const documents = this.mediaData.documents || []; |
| 1378 | + const fallbackDocumentTitle = __('Trip Document', 'yatra'); | |
| 1238 | 1379 | sideImages.forEach((item, index) => { |
| 1239 | 1380 | const img = item.querySelector('img'); |
| 1240 | 1381 | if (img && documents[index + 1]) { |
| 1241 | 1382 | const doc = documents[index + 1]; |
| 1242 | 1383 | // Keep existing image for documents |
| 1243 | - img.alt = doc.title || 'Trip Document'; | |
| 1384 | + img.alt = doc.title || fallbackDocumentTitle; | |
| 1244 | 1385 | } |
| 1245 | 1386 | }); |
| 1246 | 1387 | } |
| 1247 | 1388 | |
| @@ -1354,10 +1495,29 @@ | ||
| 1354 | 1495 | } |
| 1355 | 1496 | } |
| 1356 | 1497 | |
| 1357 | 1498 | initDateField() { |
| 1358 | - if (!this.dateInput || typeof flatpickr === 'undefined') return; | |
| 1499 | + if (!this.dateInput) return; | |
| 1359 | 1500 | |
| 1501 | + // Pro "date-as-dropdown" mode renders a <select> instead of a flatpickr | |
| 1502 | + // input. Wire it up as a native dropdown — emit `change` on selection so | |
| 1503 | + // the existing Check Availability listener picks the date up — and skip | |
| 1504 | + // the rest of the flatpickr setup. | |
| 1505 | + if (this.dateInput.tagName === 'SELECT') { | |
| 1506 | + const setEnabled = () => { | |
| 1507 | + if (this.checkAvailabilityBtn) { | |
| 1508 | + const hasValue = !!this.dateInput.value; | |
| 1509 | + this.checkAvailabilityBtn.disabled = !hasValue; | |
| 1510 | + this.checkAvailabilityBtn.classList.toggle('disabled', !hasValue); | |
| 1511 | + } | |
| 1512 | + }; | |
| 1513 | + setEnabled(); | |
| 1514 | + this.dateInput.addEventListener('change', setEnabled); | |
| 1515 | + return; | |
| 1516 | + } | |
| 1517 | + | |
| 1518 | + if (typeof flatpickr === 'undefined') return; | |
| 1519 | + | |
| 1360 | 1520 | const today = new Date(); |
| 1361 | 1521 | today.setHours(0, 0, 0, 0); |
| 1362 | 1522 | |
| 1363 | 1523 | // Format today's date as Y-m-d |
| @@ -1366,9 +1526,9 @@ | ||
| 1366 | 1526 | String(today.getDate()).padStart(2, '0'); |
| 1367 | 1527 | |
| 1368 | 1528 | // Set today's date as default value |
| 1369 | 1529 | this.dateInput.value = todayStr; |
| 1370 | - | |
| 1530 | + | |
| 1371 | 1531 | // Enable button since we have a default date |
| 1372 | 1532 | if (this.checkAvailabilityBtn) { |
| 1373 | 1533 | this.checkAvailabilityBtn.disabled = false; |
| 1374 | 1534 | this.checkAvailabilityBtn.classList.remove('disabled'); |
| @@ -1393,11 +1553,9 @@ | ||
| 1393 | 1553 | allowInput: false, |
| 1394 | 1554 | static: false, |
| 1395 | 1555 | monthSelectorType: 'static', |
| 1396 | 1556 | animate: true, |
| 1397 | - locale: { | |
| 1398 | - firstDayOfWeek: 1 | |
| 1399 | - }, | |
| 1557 | + locale: getFlatpickrLocale(), | |
| 1400 | 1558 | onChange: (selectedDates, dateStr) => { |
| 1401 | 1559 | this.dateInput.value = dateStr; |
| 1402 | 1560 | |
| 1403 | 1561 | // Enable Check Availability button when date is selected |
| @@ -1451,10 +1609,10 @@ | ||
| 1451 | 1609 | if (!minusBtn || !plusBtn) return; |
| 1452 | 1610 | |
| 1453 | 1611 | const updateButtonStates = () => { |
| 1454 | 1612 | const current = parseInt(input.value) || 1; |
| 1455 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 1456 | - const max = parseInt(input.getAttribute('max')) || 20; | |
| 1613 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 1614 | + const max = window.yatraNumOr(input.getAttribute('max'), 20); | |
| 1457 | 1615 | |
| 1458 | 1616 | minusBtn.disabled = current <= min; |
| 1459 | 1617 | plusBtn.disabled = current >= max; |
| 1460 | 1618 | }; |
| @@ -1464,9 +1622,9 @@ | ||
| 1464 | 1622 | e.preventDefault(); |
| 1465 | 1623 | e.stopPropagation(); |
| 1466 | 1624 | |
| 1467 | 1625 | const current = parseInt(input.value) || 1; |
| 1468 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 1626 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 1469 | 1627 | |
| 1470 | 1628 | if (current > min) { |
| 1471 | 1629 | input.value = current - 1; |
| 1472 | 1630 | updateButtonStates(); |
| @@ -1479,9 +1637,9 @@ | ||
| 1479 | 1637 | e.preventDefault(); |
| 1480 | 1638 | e.stopPropagation(); |
| 1481 | 1639 | |
| 1482 | 1640 | const current = parseInt(input.value) || 1; |
| 1483 | - const max = parseInt(input.getAttribute('max')) || 20; | |
| 1641 | + const max = window.yatraNumOr(input.getAttribute('max'), 20); | |
| 1484 | 1642 | |
| 1485 | 1643 | if (current < max) { |
| 1486 | 1644 | input.value = current + 1; |
| 1487 | 1645 | updateButtonStates(); |
| @@ -1555,21 +1713,25 @@ | ||
| 1555 | 1713 | const parts = []; |
| 1556 | 1714 | travelerInputs.forEach((input) => { |
| 1557 | 1715 | const value = parseInt(input.value || 0); |
| 1558 | 1716 | if (value > 0) { |
| 1559 | - const categoryLabel = input.getAttribute('data-category-label') || 'Traveler'; | |
| 1717 | + const categoryLabel = input.getAttribute('data-category-label') || __('Traveler', 'yatra'); | |
| 1560 | 1718 | parts.push(`${categoryLabel} x ${value}`); |
| 1561 | 1719 | } |
| 1562 | 1720 | }); |
| 1563 | 1721 | |
| 1564 | 1722 | if (parts.length === 0) { |
| 1565 | - // Default to first category with value 1 | |
| 1723 | + // Default to first category with value 1. `data-category-label` | |
| 1724 | + // is rendered server-side via the PHP textdomain, so it's | |
| 1725 | + // already translated. The "x 1" suffix is the only piece | |
| 1726 | + // we need to keep translatable here. | |
| 1566 | 1727 | const firstInput = travelerInputs[0]; |
| 1567 | 1728 | if (firstInput) { |
| 1568 | - const categoryLabel = firstInput.getAttribute('data-category-label') || 'Traveler'; | |
| 1569 | - display.textContent = `${categoryLabel} x 1`; | |
| 1729 | + const categoryLabel = firstInput.getAttribute('data-category-label') || __('Traveler', 'yatra'); | |
| 1730 | + /* translators: %s is the traveler-category label (e.g. "Adult", "Child"). Renders as "Adult x 1". */ | |
| 1731 | + display.textContent = __('%s x 1', 'yatra').replace('%s', categoryLabel); | |
| 1570 | 1732 | } else { |
| 1571 | - display.textContent = '1 Traveler'; | |
| 1733 | + display.textContent = __('1 Traveler', 'yatra'); | |
| 1572 | 1734 | } |
| 1573 | 1735 | } else { |
| 1574 | 1736 | display.textContent = parts.join(', '); |
| 1575 | 1737 | } |
| @@ -1663,9 +1825,9 @@ | ||
| 1663 | 1825 | categoryInputs.forEach((catInput) => { |
| 1664 | 1826 | const catValue = parseInt(catInput.value) || 0; |
| 1665 | 1827 | if (catValue > 0) { |
| 1666 | 1828 | const catRow = catInput.closest('.yatra-quantity-row'); |
| 1667 | - const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 1829 | + const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 1668 | 1830 | parts.push(`${categoryLabel} x ${catValue}`); |
| 1669 | 1831 | } |
| 1670 | 1832 | }); |
| 1671 | 1833 | if (parts.length > 0) { |
| @@ -1740,14 +1902,14 @@ | ||
| 1740 | 1902 | |
| 1741 | 1903 | |
| 1742 | 1904 | // Basic validation |
| 1743 | 1905 | if (!date) { |
| 1744 | - alert('Please select a travel date'); | |
| 1906 | + alert(__('Please select a travel date', 'yatra')); | |
| 1745 | 1907 | return; |
| 1746 | 1908 | } |
| 1747 | 1909 | |
| 1748 | 1910 | if (totalTravelers === 0) { |
| 1749 | - alert('Please select number of travelers'); | |
| 1911 | + alert(__('Please select number of travelers', 'yatra')); | |
| 1750 | 1912 | return; |
| 1751 | 1913 | } |
| 1752 | 1914 | |
| 1753 | 1915 | // Show loading state |
| @@ -1754,9 +1916,17 @@ | ||
| 1754 | 1916 | if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) { |
| 1755 | 1917 | this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML; |
| 1756 | 1918 | } |
| 1757 | 1919 | if (this.checkAvailabilityBtn) { |
| 1758 | - this.checkAvailabilityBtn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> Checking...'; | |
| 1920 | + // Spinner SVG is fixed markup (not translatable). Only the | |
| 1921 | + // user-visible "Checking..." label runs through __() so it | |
| 1922 | + // ends up in the .pot with a clean msgid and translates with | |
| 1923 | + // the rest of the UI. | |
| 1924 | + this.checkAvailabilityBtn.innerHTML = | |
| 1925 | + '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24">' + | |
| 1926 | + '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle>' + | |
| 1927 | + '<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>' + | |
| 1928 | + '</svg> ' + __('Checking...', 'yatra'); | |
| 1759 | 1929 | this.checkAvailabilityBtn.disabled = true; |
| 1760 | 1930 | } |
| 1761 | 1931 | |
| 1762 | 1932 | // Get trip ID from data attribute or global variable |
| @@ -1796,11 +1966,12 @@ | ||
| 1796 | 1966 | if (!response.ok) { |
| 1797 | 1967 | // Try to get error message from response |
| 1798 | 1968 | return response.json().then(err => { |
| 1799 | 1969 | console.error('API Error:', err); |
| 1800 | - throw new Error(err.message || 'Failed to fetch availability'); | |
| 1970 | + throw new Error(err.message || __('Failed to fetch availability', 'yatra')); | |
| 1801 | 1971 | }).catch(() => { |
| 1802 | - throw new Error('Failed to fetch availability (Status: ' + response.status + ')'); | |
| 1972 | + // translators: %d: HTTP status code | |
| 1973 | + throw new Error(sprintf(__('Failed to fetch availability (Status: %d)', 'yatra'), response.status)); | |
| 1803 | 1974 | }); |
| 1804 | 1975 | } |
| 1805 | 1976 | return response.json(); |
| 1806 | 1977 | }) |
| @@ -1864,9 +2035,9 @@ | ||
| 1864 | 2035 | this.resetButton(); |
| 1865 | 2036 | }) |
| 1866 | 2037 | .catch(error => { |
| 1867 | 2038 | console.error('Error loading availability:', error); |
| 1868 | - alert('Failed to load availability. Please try again.'); | |
| 2039 | + alert(__('Failed to load availability. Please try again.', 'yatra')); | |
| 1869 | 2040 | this.resetButton(); |
| 1870 | 2041 | }); |
| 1871 | 2042 | } |
| 1872 | 2043 | |
| @@ -1915,13 +2086,14 @@ | ||
| 1915 | 2086 | // Create new menu item |
| 1916 | 2087 | const newItem = document.createElement('a'); |
| 1917 | 2088 | newItem.href = '#availability'; |
| 1918 | 2089 | newItem.className = 'yatra-sticky-nav-item'; |
| 2090 | + const availabilityLabel = __('Availability', 'yatra'); | |
| 1919 | 2091 | newItem.innerHTML = ` |
| 1920 | 2092 | <svg class="yatra-icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 1921 | 2093 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/> |
| 1922 | 2094 | </svg> |
| 1923 | - <span>Availability</span> | |
| 2095 | + <span>${availabilityLabel}</span> | |
| 1924 | 2096 | `; |
| 1925 | 2097 | |
| 1926 | 2098 | // Insert before the target item (or after "What's Included" if no target) |
| 1927 | 2099 | if (insertBeforeItem && insertBeforeItem.parentNode) { |
| @@ -1988,9 +2160,9 @@ | ||
| 1988 | 2160 | if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) { |
| 1989 | 2161 | this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML; |
| 1990 | 2162 | } |
| 1991 | 2163 | if (this.checkAvailabilityBtn) { |
| 1992 | - this.checkAvailabilityBtn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> Loading...'; | |
| 2164 | + this.checkAvailabilityBtn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ' + __('Loading...', 'yatra'); | |
| 1993 | 2165 | this.checkAvailabilityBtn.disabled = true; |
| 1994 | 2166 | } |
| 1995 | 2167 | |
| 1996 | 2168 | const availSection = document.getElementById('availability'); |
| @@ -2014,9 +2186,9 @@ | ||
| 2014 | 2186 | credentials: 'omit', |
| 2015 | 2187 | }) |
| 2016 | 2188 | .then(response => { |
| 2017 | 2189 | if (!response.ok) { |
| 2018 | - throw new Error('Failed to fetch availability'); | |
| 2190 | + throw new Error(__('Failed to fetch availability', 'yatra')); | |
| 2019 | 2191 | } |
| 2020 | 2192 | return response.json(); |
| 2021 | 2193 | }) |
| 2022 | 2194 | .then(data => { |
| @@ -2051,9 +2223,9 @@ | ||
| 2051 | 2223 | this.resetButton(); |
| 2052 | 2224 | }) |
| 2053 | 2225 | .catch(error => { |
| 2054 | 2226 | console.error('Error reloading availability:', error); |
| 2055 | - alert('Failed to reload availability. Please try again.'); | |
| 2227 | + alert(__('Failed to reload availability. Please try again.', 'yatra')); | |
| 2056 | 2228 | this.resetButton(); |
| 2057 | 2229 | }); |
| 2058 | 2230 | } |
| 2059 | 2231 | |
| @@ -2151,9 +2323,13 @@ | ||
| 2151 | 2323 | if (data && data.success) { |
| 2152 | 2324 | // Update departures available text |
| 2153 | 2325 | const departuresText = document.querySelector('.yatra-departures-available-text'); |
| 2154 | 2326 | if (departuresText && data.departures_count !== undefined) { |
| 2155 | - departuresText.textContent = data.departures_count + ' departure' + (data.departures_count !== 1 ? 's' : '') + ' available'; | |
| 2327 | + departuresText.textContent = sprintf( | |
| 2328 | + /* translators: %d: number of departures available. */ | |
| 2329 | + _n('%d departure available', '%d departures available', data.departures_count, 'yatra'), | |
| 2330 | + data.departures_count | |
| 2331 | + ); | |
| 2156 | 2332 | } |
| 2157 | 2333 | |
| 2158 | 2334 | // Update traveler dropdown HTML |
| 2159 | 2335 | const quantitySelector = document.getElementById('quantity-selector'); |
| @@ -2207,15 +2383,15 @@ | ||
| 2207 | 2383 | const row = travelerSelector.querySelector('.yatra-quantity-row[data-category-id="' + categoryId + '"]'); |
| 2208 | 2384 | if (row) { |
| 2209 | 2385 | const cardInput = row.querySelector('input[type="number"]'); |
| 2210 | 2386 | if (cardInput) { |
| 2211 | - const max = parseInt(cardInput.getAttribute('max')) || 99; | |
| 2387 | + const max = window.yatraNumOr(cardInput.getAttribute('max'), 99); | |
| 2212 | 2388 | cardInput.value = Math.min(value, max); |
| 2213 | 2389 | |
| 2214 | 2390 | // Update button states |
| 2215 | 2391 | const minusBtn = row.querySelector('.yatra-quantity-minus'); |
| 2216 | 2392 | const plusBtn = row.querySelector('.yatra-quantity-plus'); |
| 2217 | - const min = parseInt(cardInput.getAttribute('min')) || 0; | |
| 2393 | + const min = window.yatraNumOr(cardInput.getAttribute('min'), 0); | |
| 2218 | 2394 | if (minusBtn) minusBtn.disabled = cardInput.value <= min; |
| 2219 | 2395 | if (plusBtn) plusBtn.disabled = cardInput.value >= max; |
| 2220 | 2396 | } |
| 2221 | 2397 | } |
| @@ -2226,9 +2402,9 @@ | ||
| 2226 | 2402 | |
| 2227 | 2403 | } else if (simpleTravelers && numTravelersInput) { |
| 2228 | 2404 | // Simple pricing - sync total count |
| 2229 | 2405 | const totalCount = parseInt(numTravelersInput.value) || 1; |
| 2230 | - const max = parseInt(simpleTravelers.getAttribute('max')) || 99; | |
| 2406 | + const max = window.yatraNumOr(simpleTravelers.getAttribute('max'), 99); | |
| 2231 | 2407 | simpleTravelers.value = Math.min(totalCount, max); |
| 2232 | 2408 | |
| 2233 | 2409 | // Update button states |
| 2234 | 2410 | const controls = simpleTravelers.closest('.yatra-quantity-controls-inline'); |
| @@ -2234,9 +2410,9 @@ | ||
| 2234 | 2410 | const controls = simpleTravelers.closest('.yatra-quantity-controls-inline'); |
| 2235 | 2411 | if (controls) { |
| 2236 | 2412 | const minusBtn = controls.querySelector('.yatra-quantity-minus'); |
| 2237 | 2413 | const plusBtn = controls.querySelector('.yatra-quantity-plus'); |
| 2238 | - const min = parseInt(simpleTravelers.getAttribute('min')) || 1; | |
| 2414 | + const min = window.yatraNumOr(simpleTravelers.getAttribute('min'), 1); | |
| 2239 | 2415 | if (minusBtn) minusBtn.disabled = simpleTravelers.value <= min; |
| 2240 | 2416 | if (plusBtn) plusBtn.disabled = simpleTravelers.value >= max; |
| 2241 | 2417 | } |
| 2242 | 2418 | } |
| @@ -2269,9 +2445,9 @@ | ||
| 2269 | 2445 | } |
| 2270 | 2446 | } |
| 2271 | 2447 | }); |
| 2272 | 2448 | |
| 2273 | - display.textContent = parts.length > 0 ? parts.join(', ') : '0 Travelers'; | |
| 2449 | + display.textContent = parts.length > 0 ? parts.join(', ') : __('0 Travelers', 'yatra'); | |
| 2274 | 2450 | } |
| 2275 | 2451 | |
| 2276 | 2452 | /** |
| 2277 | 2453 | * Update availability card total price |
| @@ -2299,13 +2475,17 @@ | ||
| 2299 | 2475 | const input = row.querySelector('input[type="number"]'); |
| 2300 | 2476 | const price = parseFloat(row.getAttribute('data-price')) || 0; |
| 2301 | 2477 | const count = parseInt(input?.value) || 0; |
| 2302 | 2478 | const pricingMode = row.getAttribute('data-pricing-mode') || (input ? input.getAttribute('data-pricing-mode') : '') || 'per_person'; |
| 2303 | - | |
| 2479 | + const groupOverflow = row.getAttribute('data-group-overflow') || (input ? input.getAttribute('data-group-overflow') : '') || 'block'; | |
| 2480 | + const maxPax = parseInt(row.getAttribute('data-max-pax') || (input ? input.getAttribute('data-max-pax') : '')) || 0; | |
| 2481 | + | |
| 2304 | 2482 | if (pricingMode === 'per_group') { |
| 2305 | - // Per group: charge flat price once if any travelers | |
| 2483 | + // Per group: one flat price, or per block of maxPax when "per_block". | |
| 2306 | 2484 | if (count > 0) { |
| 2307 | - total += price; | |
| 2485 | + total += (groupOverflow === 'per_block' && maxPax > 0) | |
| 2486 | + ? price * Math.ceil(count / maxPax) | |
| 2487 | + : price; | |
| 2308 | 2488 | } |
| 2309 | 2489 | } else { |
| 2310 | 2490 | // Per person: charge per traveler |
| 2311 | 2491 | total += price * count; |
| @@ -2334,9 +2514,10 @@ | ||
| 2334 | 2514 | totalAmountEl.textContent = String(total.toFixed(2)); |
| 2335 | 2515 | } |
| 2336 | 2516 | |
| 2337 | 2517 | if (totalNoteEl) { |
| 2338 | - totalNoteEl.textContent = 'for ' + totalTravelers + ' traveler' + (totalTravelers !== 1 ? 's' : ''); | |
| 2518 | + /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */ | |
| 2519 | + totalNoteEl.textContent = sprintf(_n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'), totalTravelers); | |
| 2339 | 2520 | } |
| 2340 | 2521 | |
| 2341 | 2522 | // Update the card participants display |
| 2342 | 2523 | this.updateAvailabilityCardDisplay(itemId); |
| @@ -2481,11 +2662,9 @@ | ||
| 2481 | 2662 | allowInput: false, |
| 2482 | 2663 | static: false, |
| 2483 | 2664 | monthSelectorType: 'static', |
| 2484 | 2665 | animate: true, |
| 2485 | - locale: { | |
| 2486 | - firstDayOfWeek: 1 | |
| 2487 | - }, | |
| 2666 | + locale: getFlatpickrLocale(), | |
| 2488 | 2667 | onChange: (selectedDates, dateStr) => { |
| 2489 | 2668 | this.dateInput.value = dateStr; |
| 2490 | 2669 | } |
| 2491 | 2670 | }); |
| @@ -2539,17 +2718,22 @@ | ||
| 2539 | 2718 | // Update display function |
| 2540 | 2719 | const updateDisplay = () => { |
| 2541 | 2720 | const adults = parseInt(adultsInput.value || 1); |
| 2542 | 2721 | const children = parseInt(childrenInput.value || 0); |
| 2722 | + // translators: %d: number of adults | |
| 2723 | + const adultPart = sprintf(_n('Adult x %d', 'Adult x %d', adults, 'yatra'), adults); | |
| 2724 | + // translators: %d: number of children | |
| 2725 | + const childPart = sprintf(_n('Child x %d', 'Child x %d', children, 'yatra'), children); | |
| 2543 | 2726 | let text = ''; |
| 2544 | 2727 | if (adults > 0 && children > 0) { |
| 2545 | - text = `Adult x ${adults}, Child x ${children}`; | |
| 2728 | + // translators: %1$s: adult count phrase, %2$s: child count phrase | |
| 2729 | + text = sprintf(__('%1$s, %2$s', 'yatra'), adultPart, childPart); | |
| 2546 | 2730 | } else if (adults > 0) { |
| 2547 | - text = `Adult x ${adults}`; | |
| 2731 | + text = adultPart; | |
| 2548 | 2732 | } else if (children > 0) { |
| 2549 | - text = `Child x ${children}`; | |
| 2733 | + text = childPart; | |
| 2550 | 2734 | } else { |
| 2551 | - text = 'Adult x 1'; | |
| 2735 | + text = sprintf(_n('Adult x %d', 'Adult x %d', 1, 'yatra'), 1); | |
| 2552 | 2736 | } |
| 2553 | 2737 | display.textContent = text; |
| 2554 | 2738 | }; |
| 2555 | 2739 | |
| @@ -2635,9 +2819,9 @@ | ||
| 2635 | 2819 | const email = formData.get('email'); |
| 2636 | 2820 | const message = formData.get('message'); |
| 2637 | 2821 | |
| 2638 | 2822 | if (!name || !email || !message) { |
| 2639 | - this.showMessage('error', 'Please fill in all required fields.'); | |
| 2823 | + this.showMessage('error', __('Please fill in all required fields.', 'yatra')); | |
| 2640 | 2824 | return; |
| 2641 | 2825 | } |
| 2642 | 2826 | |
| 2643 | 2827 | // Basic email validation |
| @@ -2642,22 +2826,22 @@ | ||
| 2642 | 2826 | |
| 2643 | 2827 | // Basic email validation |
| 2644 | 2828 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
| 2645 | 2829 | if (!emailRegex.test(email)) { |
| 2646 | - this.showMessage('error', 'Please enter a valid email address.'); | |
| 2830 | + this.showMessage('error', __('Please enter a valid email address.', 'yatra')); | |
| 2647 | 2831 | return; |
| 2648 | 2832 | } |
| 2649 | 2833 | |
| 2650 | 2834 | if (message.length < 10) { |
| 2651 | - this.showMessage('error', 'Please enter a message (at least 10 characters).'); | |
| 2835 | + this.showMessage('error', __('Please enter a message (at least 10 characters).', 'yatra')); | |
| 2652 | 2836 | return; |
| 2653 | 2837 | } |
| 2654 | 2838 | |
| 2655 | 2839 | // Show loading |
| 2656 | 2840 | const submitBtn = this.form.querySelector('.yatra-enquiry-submit'); |
| 2657 | - const originalText = submitBtn ? submitBtn.textContent : 'Send Enquiry'; | |
| 2841 | + const originalText = submitBtn ? submitBtn.textContent : __('Send Enquiry', 'yatra'); | |
| 2658 | 2842 | if (submitBtn) { |
| 2659 | - submitBtn.textContent = 'Sending...'; | |
| 2843 | + submitBtn.textContent = __('Sending...', 'yatra'); | |
| 2660 | 2844 | submitBtn.disabled = true; |
| 2661 | 2845 | } |
| 2662 | 2846 | |
| 2663 | 2847 | // Hide any existing messages |
| @@ -2666,8 +2850,15 @@ | ||
| 2666 | 2850 | } |
| 2667 | 2851 | |
| 2668 | 2852 | // Submit via REST API (public endpoint) |
| 2669 | 2853 | const payload = Object.fromEntries(formData.entries()); |
| 2854 | + // Ensure trip context is sent even if the hidden input is missing/empty. | |
| 2855 | + if ((!payload.trip_id || String(payload.trip_id) === '0') && window.yatraTripData?.tripId) { | |
| 2856 | + payload.trip_id = String(window.yatraTripData.tripId); | |
| 2857 | + } | |
| 2858 | + if (!payload.trip_slug && window.yatraTripData?.tripSlug) { | |
| 2859 | + payload.trip_slug = String(window.yatraTripData.tripSlug); | |
| 2860 | + } | |
| 2670 | 2861 | |
| 2671 | 2862 | // Build REST URL |
| 2672 | 2863 | const baseUrlRaw = (window.yatraTripData?.apiUrl || window.yatraTripData?.restUrl || window.wpApiSettings?.root || '/wp-json').replace(/\/$/, ''); |
| 2673 | 2864 | const enquiriesUrl = baseUrlRaw.includes('/yatra/v1') |
| @@ -2673,26 +2864,38 @@ | ||
| 2673 | 2864 | const enquiriesUrl = baseUrlRaw.includes('/yatra/v1') |
| 2674 | 2865 | ? `${baseUrlRaw}/enquiries` |
| 2675 | 2866 | : `${baseUrlRaw}/yatra/v1/enquiries`; |
| 2676 | 2867 | |
| 2677 | - fetch(enquiriesUrl, { | |
| 2678 | - method: 'POST', | |
| 2679 | - headers: { | |
| 2680 | - 'Content-Type': 'application/json', | |
| 2681 | - }, | |
| 2682 | - credentials: 'omit', // public endpoint | |
| 2683 | - body: JSON.stringify(payload), | |
| 2684 | - }) | |
| 2868 | + // reCAPTCHA v3: attach a fresh token when the enquiry form is protected. | |
| 2869 | + const yatraRc = window.yatraRecaptcha; | |
| 2870 | + const enquiryTokenPromise = (yatraRc && yatraRc.protects && yatraRc.protects('enquiry')) | |
| 2871 | + ? yatraRc.execute('enquiry') | |
| 2872 | + : Promise.resolve(''); | |
| 2873 | + | |
| 2874 | + enquiryTokenPromise | |
| 2875 | + .then(recaptchaToken => { | |
| 2876 | + if (recaptchaToken) { | |
| 2877 | + payload.recaptcha_token = recaptchaToken; | |
| 2878 | + } | |
| 2879 | + return fetch(enquiriesUrl, { | |
| 2880 | + method: 'POST', | |
| 2881 | + headers: { | |
| 2882 | + 'Content-Type': 'application/json', | |
| 2883 | + }, | |
| 2884 | + credentials: 'omit', // public endpoint | |
| 2885 | + body: JSON.stringify(payload), | |
| 2886 | + }); | |
| 2887 | + }) | |
| 2685 | 2888 | .then(response => response.json()) |
| 2686 | 2889 | .then(result => { |
| 2687 | 2890 | if (result.success) { |
| 2688 | - this.showMessage('success', result.message || result.data?.message || 'Thank you for your enquiry! We will get back to you soon.'); | |
| 2891 | + this.showMessage('success', result.message || result.data?.message || __('Thank you for your enquiry! We will get back to you soon.', 'yatra')); | |
| 2689 | 2892 | this.form.reset(); |
| 2690 | 2893 | |
| 2691 | 2894 | // Reset the travelers display |
| 2692 | 2895 | const display = document.getElementById('enquiry-participants-display'); |
| 2693 | 2896 | if (display) { |
| 2694 | - display.textContent = 'Adult x 1'; | |
| 2897 | + display.textContent = __('Adult x 1', 'yatra'); | |
| 2695 | 2898 | } |
| 2696 | 2899 | const adultsInput = document.getElementById('enquiry-adults'); |
| 2697 | 2900 | const childrenInput = document.getElementById('enquiry-children'); |
| 2698 | 2901 | if (adultsInput) adultsInput.value = 1; |
| @@ -2705,14 +2908,14 @@ | ||
| 2705 | 2908 | messageBox.style.display = 'none'; |
| 2706 | 2909 | } |
| 2707 | 2910 | }, 3000); |
| 2708 | 2911 | } else { |
| 2709 | - this.showMessage('error', result.message || result.data?.message || 'Failed to submit enquiry. Please try again.'); | |
| 2912 | + this.showMessage('error', result.message || result.data?.message || __('Failed to submit enquiry. Please try again.', 'yatra')); | |
| 2710 | 2913 | } |
| 2711 | 2914 | }) |
| 2712 | 2915 | .catch(error => { |
| 2713 | 2916 | console.error('Enquiry submission error:', error); |
| 2714 | - this.showMessage('error', 'An error occurred. Please try again later.'); | |
| 2917 | + this.showMessage('error', __('An error occurred. Please try again later.', 'yatra')); | |
| 2715 | 2918 | }) |
| 2716 | 2919 | .finally(() => { |
| 2717 | 2920 | if (submitBtn) { |
| 2718 | 2921 | submitBtn.textContent = originalText; |
| @@ -2777,21 +2980,33 @@ | ||
| 2777 | 2980 | this.updateSidebarPosition(); // Initial check |
| 2778 | 2981 | } |
| 2779 | 2982 | |
| 2780 | 2983 | getActualSections() { |
| 2781 | - // Base sections that are always present in order | |
| 2782 | - const baseSections = ['overview', 'itinerary', 'included', 'location', 'important-info']; | |
| 2783 | - | |
| 2784 | - // Conditional sections that may or may not be present | |
| 2785 | - const conditionalSections = ['downloads', 'faq', 'trip-story', 'what-makes-special', 'testimonials']; | |
| 2786 | - | |
| 2787 | - // Combine all sections and filter to only include those that actually exist on the page | |
| 2788 | - const allSections = [...baseSections, ...conditionalSections]; | |
| 2789 | - | |
| 2790 | - return allSections.filter(sectionId => { | |
| 2791 | - const element = document.getElementById(sectionId); | |
| 2792 | - return element && element.offsetParent !== null; // Check if element exists and is visible | |
| 2984 | + // Derive section IDs from the sticky nav items themselves rather | |
| 2985 | + // than hardcoding the list. The previous hardcoded version only knew | |
| 2986 | + // about the built-in sections (overview, itinerary, included, …) so | |
| 2987 | + // admin-defined CUSTOM tabs — whose ID is `custom_<timestamp>` — were | |
| 2988 | + // silently dropped: their nav-link still scrolled, but updateActiveNav | |
| 2989 | + // never highlighted them as you scrolled past, and the "is this | |
| 2990 | + // section on-screen" check below was always false for them. | |
| 2991 | + // | |
| 2992 | + // Reading from this.navItems means every enabled tab (built-in OR | |
| 2993 | + // custom) participates in active-state tracking without needing this | |
| 2994 | + // list updated for each new tab type. | |
| 2995 | + const seen = new Set(); | |
| 2996 | + const ids = []; | |
| 2997 | + this.navItems.forEach((item) => { | |
| 2998 | + const href = item.getAttribute('href'); | |
| 2999 | + if (!href || !href.startsWith('#')) return; | |
| 3000 | + const id = href.substring(1); | |
| 3001 | + if (!id || seen.has(id)) return; | |
| 3002 | + const element = document.getElementById(id); | |
| 3003 | + if (element && element.offsetParent !== null) { | |
| 3004 | + seen.add(id); | |
| 3005 | + ids.push(id); | |
| 3006 | + } | |
| 2793 | 3007 | }); |
| 3008 | + return ids; | |
| 2794 | 3009 | } |
| 2795 | 3010 | |
| 2796 | 3011 | attachEventListeners() { |
| 2797 | 3012 | // Handle clicks on nav items |
| @@ -2963,8 +3178,15 @@ | ||
| 2963 | 3178 | this.lastScrollTop = scrollTop; |
| 2964 | 3179 | } |
| 2965 | 3180 | |
| 2966 | 3181 | updateActiveNav() { |
| 3182 | + // `offsetTop` is relative to the closest positioned ancestor, not the | |
| 3183 | + // document. When a section lives inside a `position: relative` wrapper | |
| 3184 | + // (any of the layout containers on this page), offsetTop is wrong by | |
| 3185 | + // the wrapper's offset, so the "active" pill never lit up correctly | |
| 3186 | + // for some tabs — most visibly the newly-added custom tabs that sit | |
| 3187 | + // further down the DOM. `getBoundingClientRect().top + pageYOffset` | |
| 3188 | + // always gives the document-relative top regardless of ancestors. | |
| 2967 | 3189 | const scrollPos = window.scrollY + 150; |
| 2968 | 3190 | let activeSectionId = null; |
| 2969 | 3191 | |
| 2970 | 3192 | // Find which section is currently in view |
| @@ -2970,10 +3192,11 @@ | ||
| 2970 | 3192 | // Find which section is currently in view |
| 2971 | 3193 | this.sections.forEach((sectionId) => { |
| 2972 | 3194 | const section = document.getElementById(sectionId); |
| 2973 | 3195 | if (section) { |
| 2974 | - const sectionTop = section.offsetTop; | |
| 2975 | - const sectionBottom = sectionTop + section.offsetHeight; | |
| 3196 | + const rect = section.getBoundingClientRect(); | |
| 3197 | + const sectionTop = rect.top + window.pageYOffset; | |
| 3198 | + const sectionBottom = sectionTop + rect.height; | |
| 2976 | 3199 | |
| 2977 | 3200 | if (scrollPos >= sectionTop && scrollPos < sectionBottom) { |
| 2978 | 3201 | activeSectionId = sectionId; |
| 2979 | 3202 | } |
| @@ -3120,14 +3343,14 @@ | ||
| 3120 | 3343 | } |
| 3121 | 3344 | } |
| 3122 | 3345 | |
| 3123 | 3346 | if (travelers < 1) { |
| 3124 | - this.showBookingError(btn, 'Please select at least 1 traveler to continue.'); | |
| 3347 | + this.showBookingError(btn, __('Please select at least 1 traveler to continue.', 'yatra')); | |
| 3125 | 3348 | return; |
| 3126 | 3349 | } |
| 3127 | 3350 | |
| 3128 | 3351 | const originalText = btn.innerHTML; |
| 3129 | - btn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> Processing...'; | |
| 3352 | + btn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ' + __('Processing...', 'yatra'); | |
| 3130 | 3353 | btn.disabled = true; |
| 3131 | 3354 | |
| 3132 | 3355 | if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { |
| 3133 | 3356 | console.error('Invalid trip ID:', tripId); |
| @@ -3132,9 +3355,9 @@ | ||
| 3132 | 3355 | if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { |
| 3133 | 3356 | console.error('Invalid trip ID:', tripId); |
| 3134 | 3357 | btn.innerHTML = originalText; |
| 3135 | 3358 | btn.disabled = false; |
| 3136 | - this.showBookingError(btn, 'Unable to process booking. Invalid trip data.'); | |
| 3359 | + this.showBookingError(btn, __('Unable to process booking. Invalid trip data.', 'yatra')); | |
| 3137 | 3360 | return; |
| 3138 | 3361 | } |
| 3139 | 3362 | |
| 3140 | 3363 | const pricingType = btn.getAttribute('data-pricing-type') || 'regular'; |
| @@ -3355,9 +3578,9 @@ | ||
| 3355 | 3578 | currency: window.yatraBookingData?.currency || 'USD', |
| 3356 | 3579 | num_items: sessionPayload.travelers || 1 |
| 3357 | 3580 | }); |
| 3358 | 3581 | |
| 3359 | - console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id); | |
| 3582 | + window.YATRA_DEBUG && console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id); | |
| 3360 | 3583 | } |
| 3361 | 3584 | |
| 3362 | 3585 | // Track Book Now click as begin_checkout (Google Analytics 4) |
| 3363 | 3586 | if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) { |
| @@ -3375,9 +3598,9 @@ | ||
| 3375 | 3598 | quantity: sessionPayload.travelers || 1 |
| 3376 | 3599 | }] |
| 3377 | 3600 | }); |
| 3378 | 3601 | |
| 3379 | - console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id); | |
| 3602 | + window.YATRA_DEBUG && console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id); | |
| 3380 | 3603 | } |
| 3381 | 3604 | |
| 3382 | 3605 | // Set booking session via REST API |
| 3383 | 3606 | // credentials: 'same-origin' is required to send cookies for session |
| @@ -3408,9 +3631,9 @@ | ||
| 3408 | 3631 | // Error - show message and reset button |
| 3409 | 3632 | console.error('Booking session error:', data); |
| 3410 | 3633 | btn.innerHTML = originalText; |
| 3411 | 3634 | btn.disabled = false; |
| 3412 | - this.showBookingError(btn, data.message || 'Unable to process booking. Please try again.'); | |
| 3635 | + this.showBookingError(btn, data.message || __('Unable to process booking. Please try again.', 'yatra')); | |
| 3413 | 3636 | } |
| 3414 | 3637 | }) |
| 3415 | 3638 | .catch(error => { |
| 3416 | 3639 | console.error('Error setting booking session:', error); |
| @@ -3415,9 +3638,9 @@ | ||
| 3415 | 3638 | .catch(error => { |
| 3416 | 3639 | console.error('Error setting booking session:', error); |
| 3417 | 3640 | btn.innerHTML = originalText; |
| 3418 | 3641 | btn.disabled = false; |
| 3419 | - this.showBookingError(btn, 'Unable to process booking. Please try again.'); | |
| 3642 | + this.showBookingError(btn, __('Unable to process booking. Please try again.', 'yatra')); | |
| 3420 | 3643 | }); |
| 3421 | 3644 | } |
| 3422 | 3645 | |
| 3423 | 3646 | initTravelerSelectors() { |
| @@ -3449,18 +3672,21 @@ | ||
| 3449 | 3672 | categoryInputs.forEach((input) => { |
| 3450 | 3673 | const value = parseInt(input.value) || 0; |
| 3451 | 3674 | if (value > 0) { |
| 3452 | 3675 | const row = input.closest('.yatra-quantity-row'); |
| 3453 | - const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3676 | + const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3454 | 3677 | parts.push(`${categoryLabel} x ${value}`); |
| 3455 | 3678 | } |
| 3456 | 3679 | }); |
| 3457 | 3680 | |
| 3458 | 3681 | if (parts.length === 0) { |
| 3459 | - // Default to first category | |
| 3682 | + // Default to first category. The label is read from the | |
| 3683 | + // DOM (server-rendered → already translated). Only the | |
| 3684 | + // " x 1" suffix is the literal we have to localise here. | |
| 3460 | 3685 | const firstRow = categoryInputs[0]?.closest('.yatra-quantity-row'); |
| 3461 | - const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3462 | - display.textContent = `${firstLabel} x 1`; | |
| 3686 | + const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3687 | + /* translators: %s is the traveler-category label (e.g. "Adult", "Child"). Renders as "Adult x 1". */ | |
| 3688 | + display.textContent = __('%s x 1', 'yatra').replace('%s', firstLabel); | |
| 3463 | 3689 | } else { |
| 3464 | 3690 | display.textContent = parts.join(', '); |
| 3465 | 3691 | } |
| 3466 | 3692 | }; |
| @@ -3482,10 +3708,10 @@ | ||
| 3482 | 3708 | const isMinus = btn.classList.contains('yatra-quantity-minus'); |
| 3483 | 3709 | const isPlus = btn.classList.contains('yatra-quantity-plus'); |
| 3484 | 3710 | |
| 3485 | 3711 | let current = parseInt(input.value) || 0; |
| 3486 | - const min = parseInt(input.getAttribute('min')) || 0; | |
| 3487 | - const max = parseInt(input.getAttribute('max')) || 999; | |
| 3712 | + const min = window.yatraNumOr(input.getAttribute('min'), 0); | |
| 3713 | + const max = window.yatraNumOr(input.getAttribute('max'), 999); | |
| 3488 | 3714 | let newValue = current; |
| 3489 | 3715 | |
| 3490 | 3716 | if (isPlus && current < max) { |
| 3491 | 3717 | newValue = current + 1; |
| @@ -3557,10 +3783,10 @@ | ||
| 3557 | 3783 | const isMinus = btn.classList.contains('yatra-quantity-minus'); |
| 3558 | 3784 | const isPlus = btn.classList.contains('yatra-quantity-plus'); |
| 3559 | 3785 | |
| 3560 | 3786 | let current = parseInt(input.value) || 1; |
| 3561 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 3562 | - const max = parseInt(input.getAttribute('max')) || 999; | |
| 3787 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 3788 | + const max = window.yatraNumOr(input.getAttribute('max'), 999); | |
| 3563 | 3789 | let newValue = current; |
| 3564 | 3790 | |
| 3565 | 3791 | if (isPlus && current < max) { |
| 3566 | 3792 | newValue = current + 1; |
| @@ -3632,9 +3858,9 @@ | ||
| 3632 | 3858 | categoryInputs.forEach((input) => { |
| 3633 | 3859 | const value = parseInt(input.value) || 0; |
| 3634 | 3860 | if (value > 0) { |
| 3635 | 3861 | const row = input.closest('.yatra-quantity-row'); |
| 3636 | - const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3862 | + const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3637 | 3863 | parts.push(`${categoryLabel} x ${value}`); |
| 3638 | 3864 | |
| 3639 | 3865 | // Also update sidebar inputs if they exist |
| 3640 | 3866 | const categoryId = input.getAttribute('data-category'); |
| @@ -3670,8 +3896,9 @@ | ||
| 3670 | 3896 | // Check for traveler-based pricing first |
| 3671 | 3897 | const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`); |
| 3672 | 3898 | let totalPrice = 0; |
| 3673 | 3899 | let totalTravelers = 0; |
| 3900 | + let totalGroups = 0; // per-group categories: number of group prices charged | |
| 3674 | 3901 | |
| 3675 | 3902 | if (categoryInputs.length > 0) { |
| 3676 | 3903 | // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode) |
| 3677 | 3904 | categoryInputs.forEach((input) => { |
| @@ -3678,12 +3905,16 @@ | ||
| 3678 | 3905 | const quantity = parseInt(input.value) || 0; |
| 3679 | 3906 | const row = input.closest('.yatra-quantity-row'); |
| 3680 | 3907 | const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0; |
| 3681 | 3908 | const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person'; |
| 3682 | - | |
| 3909 | + const groupOverflow = (row ? row.getAttribute('data-group-overflow') : null) || input.getAttribute('data-group-overflow') || 'block'; | |
| 3910 | + const maxPax = parseInt((row ? row.getAttribute('data-max-pax') : null) || input.getAttribute('data-max-pax')) || 0; | |
| 3911 | + | |
| 3683 | 3912 | if (pricingMode === 'per_group') { |
| 3684 | 3913 | if (quantity > 0) { |
| 3685 | - totalPrice += price; | |
| 3914 | + const blocks = (groupOverflow === 'per_block' && maxPax > 0) ? Math.ceil(quantity / maxPax) : 1; | |
| 3915 | + totalPrice += price * blocks; | |
| 3916 | + totalGroups += blocks; | |
| 3686 | 3917 | } |
| 3687 | 3918 | } else { |
| 3688 | 3919 | totalPrice += quantity * price; |
| 3689 | 3920 | } |
| @@ -3711,14 +3942,21 @@ | ||
| 3711 | 3942 | } else { |
| 3712 | 3943 | totalAmountElement.textContent = String(finalPrice.toFixed(2)); |
| 3713 | 3944 | } |
| 3714 | 3945 | |
| 3715 | - // Header "per person" must match chargable average (row totals + group discount), not static PHP. | |
| 3946 | + // Header price must match the chargeable amount (row totals + group discount), not static PHP. | |
| 3947 | + // Its unit follows the card's price label: "per person" → average per traveller; | |
| 3948 | + // "per group" → the price per group actually charged (never divided by headcount, | |
| 3949 | + // which would turn a flat $1,100 group price into "$550 per group" for two travellers). | |
| 3716 | 3950 | const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]'); |
| 3717 | - const headerSaleEl = cardRoot ? cardRoot.querySelector('.yatra-card-header-price .yatra-sale-price') : null; | |
| 3951 | + const headerPriceWrap = cardRoot ? cardRoot.querySelector('.yatra-card-header-price') : null; | |
| 3952 | + const headerSaleEl = headerPriceWrap ? headerPriceWrap.querySelector('.yatra-sale-price') : null; | |
| 3718 | 3953 | if (headerSaleEl) { |
| 3954 | + const headerUnit = headerPriceWrap.getAttribute('data-price-unit') || 'per_person'; | |
| 3719 | 3955 | let perPerson = 0; |
| 3720 | - if (totalTravelers > 0) { | |
| 3956 | + if (headerUnit === 'per_group') { | |
| 3957 | + perPerson = totalGroups > 0 ? finalPrice / totalGroups : (totalPrice > 0 ? totalPrice : 0); | |
| 3958 | + } else if (totalTravelers > 0) { | |
| 3721 | 3959 | perPerson = finalPrice / totalTravelers; |
| 3722 | 3960 | } else if (totalPrice > 0) { |
| 3723 | 3961 | perPerson = totalPrice; |
| 3724 | 3962 | } |
| @@ -3732,11 +3970,13 @@ | ||
| 3732 | 3970 | const noteElement = this.section.querySelector( |
| 3733 | 3971 | `.yatra-card-total-note[data-item="${itemIndex}"]` |
| 3734 | 3972 | ); |
| 3735 | 3973 | if (noteElement) { |
| 3736 | - let travelerText = totalTravelers === 1 | |
| 3737 | - ? 'for 1 traveler' | |
| 3738 | - : `for ${totalTravelers} travelers`; | |
| 3974 | + let travelerText = sprintf( | |
| 3975 | + /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */ | |
| 3976 | + _n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'), | |
| 3977 | + totalTravelers | |
| 3978 | + ); | |
| 3739 | 3979 | |
| 3740 | 3980 | // Add group discount info if applied |
| 3741 | 3981 | if (groupDiscountApplied && groupDiscountAmount > 0) { |
| 3742 | 3982 | const discountText = (typeof yatra_format_price_js === 'function') |
| @@ -3741,9 +3981,13 @@ | ||
| 3741 | 3981 | if (groupDiscountApplied && groupDiscountAmount > 0) { |
| 3742 | 3982 | const discountText = (typeof yatra_format_price_js === 'function') |
| 3743 | 3983 | ? yatra_format_price_js(groupDiscountAmount) |
| 3744 | 3984 | : String(groupDiscountAmount.toFixed(2)); |
| 3745 | - travelerText += ` (Group discount: -${discountText})`; | |
| 3985 | + travelerText += sprintf( | |
| 3986 | + /* translators: %s: formatted discount amount. */ | |
| 3987 | + __(' (Group discount: -%s)', 'yatra'), | |
| 3988 | + discountText | |
| 3989 | + ); | |
| 3746 | 3990 | } |
| 3747 | 3991 | noteElement.textContent = travelerText; |
| 3748 | 3992 | } |
| 3749 | 3993 | |
| @@ -3858,10 +4102,14 @@ | ||
| 3858 | 4102 | } |
| 3859 | 4103 | |
| 3860 | 4104 | // Update badge content |
| 3861 | 4105 | const discountText = discountResult.discountType === 'percentage' |
| 3862 | - ? `${discountResult.discountPercentage}% group discount applied!` | |
| 3863 | - : `Group discount applied!`; | |
| 4106 | + ? sprintf( | |
| 4107 | + /* translators: %s: discount percentage. */ | |
| 4108 | + __('%s%% group discount applied!', 'yatra'), | |
| 4109 | + discountResult.discountPercentage | |
| 4110 | + ) | |
| 4111 | + : __('Group discount applied!', 'yatra'); | |
| 3864 | 4112 | badge.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> ${discountText}`; |
| 3865 | 4113 | } else if (badge) { |
| 3866 | 4114 | badge.remove(); |
| 3867 | 4115 | } |
| @@ -3941,9 +4189,9 @@ | ||
| 3941 | 4189 | } |
| 3942 | 4190 | |
| 3943 | 4191 | // Show success feedback |
| 3944 | 4192 | const originalText = button.textContent; |
| 3945 | - button.textContent = 'Selected!'; | |
| 4193 | + button.textContent = __('Selected!', 'yatra'); | |
| 3946 | 4194 | button.style.background = '#10b981'; |
| 3947 | 4195 | setTimeout(() => { |
| 3948 | 4196 | button.textContent = originalText; |
| 3949 | 4197 | button.style.background = ''; |
| @@ -3968,9 +4216,11 @@ | ||
| 3968 | 4216 | const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined; |
| 3969 | 4217 | |
| 3970 | 4218 | const originalHTML = btn.innerHTML; |
| 3971 | 4219 | const loadingSvg = '<svg class="yatra-spinner" width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> '; |
| 3972 | - btn.innerHTML = loadingSvg + 'Loading...'; | |
| 4220 | + // Spinner SVG is fixed markup; only the visible label is wrapped | |
| 4221 | + // so it ends up in the .pot and translates with the rest of the UI. | |
| 4222 | + btn.innerHTML = loadingSvg + __('Loading...', 'yatra'); | |
| 3973 | 4223 | btn.disabled = true; |
| 3974 | 4224 | |
| 3975 | 4225 | const url = buildAvailabilityTemplateUrl(tripId, { |
| 3976 | 4226 | date: travelDate, |
| @@ -3989,9 +4239,9 @@ | ||
| 3989 | 4239 | credentials: 'omit', |
| 3990 | 4240 | }) |
| 3991 | 4241 | .then((resp) => { |
| 3992 | 4242 | if (!resp.ok) { |
| 3993 | - throw new Error('Load more failed'); | |
| 4243 | + throw new Error(__('Load more failed', 'yatra')); | |
| 3994 | 4244 | } |
| 3995 | 4245 | return resp.json(); |
| 3996 | 4246 | }) |
| 3997 | 4247 | .then((data) => { |
| @@ -4028,9 +4278,11 @@ | ||
| 4028 | 4278 | return; |
| 4029 | 4279 | } |
| 4030 | 4280 | |
| 4031 | 4281 | const remaining = Math.max(0, total - displayed); |
| 4032 | - btn.innerHTML = `<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg> Load more departures (${remaining} remaining)`; | |
| 4282 | + /* translators: %d: number of additional departures still available to load. */ | |
| 4283 | + const loadMoreLabel = sprintf(__('Load more departures (%d remaining)', 'yatra'), remaining); | |
| 4284 | + btn.innerHTML = `<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg> ${loadMoreLabel}`; | |
| 4033 | 4285 | btn.disabled = false; |
| 4034 | 4286 | |
| 4035 | 4287 | loadMoreContainer.dataset.currentPage = String(nextPage); |
| 4036 | 4288 | loadMoreContainer.dataset.loaded = String(displayed); |
| @@ -4036,9 +4288,17 @@ | ||
| 4036 | 4288 | loadMoreContainer.dataset.loaded = String(displayed); |
| 4037 | 4289 | loadMoreContainer.dataset.total = String(total); |
| 4038 | 4290 | |
| 4039 | 4291 | if (countInfo) { |
| 4040 | - countInfo.textContent = `Showing ${displayed} of ${total} departures`; | |
| 4292 | + // Pagination summary. Two placeholders preserved as | |
| 4293 | + // %1$d / %2$d so translators can reorder them (some | |
| 4294 | + // locales prefer "Of N, showing M" word order). | |
| 4295 | + /* translators: %1$d is the number of departures shown so far; %2$d is the total available. */ | |
| 4296 | + countInfo.textContent = sprintf( | |
| 4297 | + __('Showing %1$d of %2$d departures', 'yatra'), | |
| 4298 | + displayed, | |
| 4299 | + total | |
| 4300 | + ); | |
| 4041 | 4301 | } |
| 4042 | 4302 | }) |
| 4043 | 4303 | .catch((err) => { |
| 4044 | 4304 | console.error('[Yatra] Load more failed:', err); |
| @@ -4202,9 +4462,9 @@ | ||
| 4202 | 4462 | if (!this.toggleAllBtn) return; |
| 4203 | 4463 | this.toggleAllBtn.classList.toggle('is-all-expanded', isExpanded); |
| 4204 | 4464 | const toggleText = this.toggleAllBtn.querySelector('.toggle-text'); |
| 4205 | 4465 | if (toggleText) { |
| 4206 | - toggleText.textContent = isExpanded ? 'Collapse All' : 'Expand All'; | |
| 4466 | + toggleText.textContent = isExpanded ? __('Collapse All', 'yatra') : __('Expand All', 'yatra'); | |
| 4207 | 4467 | } |
| 4208 | 4468 | } |
| 4209 | 4469 | } |
| 4210 | 4470 | |
| @@ -4317,9 +4577,10 @@ | ||
| 4317 | 4577 | const originalText = submitBtn.innerHTML; |
| 4318 | 4578 | |
| 4319 | 4579 | // Disable button and show loading |
| 4320 | 4580 | submitBtn.disabled = true; |
| 4321 | - submitBtn.innerHTML = '<span class="yatra-loading-spinner"></span> Submitting...'; | |
| 4581 | + submitBtn.innerHTML = | |
| 4582 | + '<span class="yatra-loading-spinner"></span> ' + __('Submitting...', 'yatra'); | |
| 4322 | 4583 | |
| 4323 | 4584 | try { |
| 4324 | 4585 | const formData = new FormData(this.form); |
| 4325 | 4586 | formData.append('action', 'yatra_submit_review'); |
| @@ -4333,9 +4594,9 @@ | ||
| 4333 | 4594 | const result = await response.json(); |
| 4334 | 4595 | |
| 4335 | 4596 | if (result.success) { |
| 4336 | 4597 | // Show success message |
| 4337 | - this.showMessage('success', result.data?.message || 'Thank you for your review!'); | |
| 4598 | + this.showMessage('success', result.data?.message || __('Thank you for your review!', 'yatra')); | |
| 4338 | 4599 | |
| 4339 | 4600 | // Check if this was an edit or new submission |
| 4340 | 4601 | const isEdit = this.form.querySelector('input[name="action_type"]')?.value === 'edit'; |
| 4341 | 4602 | |
| @@ -4352,13 +4613,13 @@ | ||
| 4352 | 4613 | ratingLabels.forEach(label => label.classList.add('selected')); |
| 4353 | 4614 | } |
| 4354 | 4615 | } |
| 4355 | 4616 | } else { |
| 4356 | - this.showMessage('error', result.data?.message || 'Failed to submit review. Please try again.'); | |
| 4617 | + this.showMessage('error', result.data?.message || __('Failed to submit review. Please try again.', 'yatra')); | |
| 4357 | 4618 | } |
| 4358 | 4619 | } catch (error) { |
| 4359 | 4620 | console.error('Review submission error:', error); |
| 4360 | - this.showMessage('error', 'An error occurred. Please try again.'); | |
| 4621 | + this.showMessage('error', __('An error occurred. Please try again.', 'yatra')); | |
| 4361 | 4622 | } finally { |
| 4362 | 4623 | submitBtn.disabled = false; |
| 4363 | 4624 | submitBtn.innerHTML = originalText; |
| 4364 | 4625 | } |
| @@ -4532,16 +4793,13 @@ | ||
| 4532 | 4793 | * Handles save/remove trip from wishlist functionality |
| 4533 | 4794 | */ |
| 4534 | 4795 | class WishlistHandler { |
| 4535 | 4796 | constructor() { |
| 4536 | - if (!window.yatraTripData?.wishlistEnabled) { | |
| 4537 | - return; | |
| 4538 | - } | |
| 4539 | - this.apiUrl = window.yatraTripData?.apiUrl || '/wp-json/yatra/v1'; | |
| 4540 | - this.nonce = window.yatraTripData?.nonce || ''; | |
| 4541 | - this.tripId = window.yatraTripData?.tripId || 0; | |
| 4542 | - this.isLoggedIn = window.yatraTripData?.isLoggedIn || false; | |
| 4543 | - this.init(); | |
| 4797 | + // Wishlist clicks are now handled exclusively by listing-wishlist.js | |
| 4798 | + // (which covers every .yatra-favorite-btn on listing, single-trip hero, | |
| 4799 | + // and similar-trips). Registering this second handler caused every click | |
| 4800 | + // to fire two REST requests racing against each other. | |
| 4801 | + return; | |
| 4544 | 4802 | } |
| 4545 | 4803 | |
| 4546 | 4804 | init() { |
| 4547 | 4805 | // Check if user is logged in and update button states |
| @@ -4624,15 +4882,15 @@ | ||
| 4624 | 4882 | |
| 4625 | 4883 | const data = await response.json(); |
| 4626 | 4884 | if (data.success) { |
| 4627 | 4885 | this.updateButtonState(true, tid); |
| 4628 | - this.showMessage(data.message || 'Trip saved to wishlist', 'success', btn); | |
| 4886 | + this.showMessage(data.message || __('Trip saved to wishlist', 'yatra'), 'success', btn); | |
| 4629 | 4887 | } else { |
| 4630 | - this.showMessage(data.message || 'Failed to save trip', 'error', btn); | |
| 4888 | + this.showMessage(data.message || __('Failed to save trip', 'yatra'), 'error', btn); | |
| 4631 | 4889 | } |
| 4632 | 4890 | } catch (error) { |
| 4633 | 4891 | console.error('Error saving trip:', error); |
| 4634 | - this.showMessage('An error occurred. Please try again.', 'error', btn); | |
| 4892 | + this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn); | |
| 4635 | 4893 | } finally { |
| 4636 | 4894 | btn.disabled = false; |
| 4637 | 4895 | btn.classList.remove('loading'); |
| 4638 | 4896 | } |
| @@ -4654,15 +4912,15 @@ | ||
| 4654 | 4912 | |
| 4655 | 4913 | const data = await response.json(); |
| 4656 | 4914 | if (data.success) { |
| 4657 | 4915 | this.updateButtonState(false, tid); |
| 4658 | - this.showMessage(data.message || 'Trip removed from wishlist', 'success', btn); | |
| 4916 | + this.showMessage(data.message || __('Trip removed from wishlist', 'yatra'), 'success', btn); | |
| 4659 | 4917 | } else { |
| 4660 | - this.showMessage(data.message || 'Failed to remove trip', 'error', btn); | |
| 4918 | + this.showMessage(data.message || __('Failed to remove trip', 'yatra'), 'error', btn); | |
| 4661 | 4919 | } |
| 4662 | 4920 | } catch (error) { |
| 4663 | 4921 | console.error('Error removing trip:', error); |
| 4664 | - this.showMessage('An error occurred. Please try again.', 'error', btn); | |
| 4922 | + this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn); | |
| 4665 | 4923 | } finally { |
| 4666 | 4924 | btn.disabled = false; |
| 4667 | 4925 | btn.classList.remove('loading'); |
| 4668 | 4926 | } |
| @@ -4676,9 +4934,9 @@ | ||
| 4676 | 4934 | const buttons = document.querySelectorAll(selector); |
| 4677 | 4935 | buttons.forEach(btn => { |
| 4678 | 4936 | if (isSaved) { |
| 4679 | 4937 | btn.classList.add('saved', 'is-saved'); |
| 4680 | - btn.setAttribute('aria-label', 'Remove from favorites'); | |
| 4938 | + btn.setAttribute('aria-label', __('Remove from favorites', 'yatra')); | |
| 4681 | 4939 | // Fill the heart icon and change color |
| 4682 | 4940 | const svg = btn.querySelector('svg'); |
| 4683 | 4941 | if (svg) { |
| 4684 | 4942 | svg.style.fill = 'currentColor'; |
| @@ -4686,9 +4944,9 @@ | ||
| 4686 | 4944 | btn.style.color = '#ef4444'; |
| 4687 | 4945 | } |
| 4688 | 4946 | } else { |
| 4689 | 4947 | btn.classList.remove('saved', 'is-saved'); |
| 4690 | - btn.setAttribute('aria-label', 'Add to favorites'); | |
| 4948 | + btn.setAttribute('aria-label', __('Add to favorites', 'yatra')); | |
| 4691 | 4949 | // Unfill the heart icon |
| 4692 | 4950 | const svg = btn.querySelector('svg'); |
| 4693 | 4951 | if (svg) { |
| 4694 | 4952 | svg.style.fill = 'none'; |
| @@ -4712,13 +4970,17 @@ | ||
| 4712 | 4970 | if (modalContent) { |
| 4713 | 4971 | const loginMessage = document.createElement('div'); |
| 4714 | 4972 | loginMessage.className = 'yatra-login-prompt'; |
| 4715 | 4973 | loginMessage.style.cssText = 'padding: 40px; text-align: center;'; |
| 4974 | + const loginRequiredHeading = __('Login Required', 'yatra'); | |
| 4975 | + const loginRequiredBody = __('Please login to save trips to your wishlist.', 'yatra'); | |
| 4976 | + const loginButtonLabel = __('Login', 'yatra'); | |
| 4977 | + const cancelButtonLabel = __('Cancel', 'yatra'); | |
| 4716 | 4978 | loginMessage.innerHTML = ` |
| 4717 | - <h3 style="margin-bottom: 20px;">Login Required</h3> | |
| 4718 | - <p style="margin-bottom: 30px;">Please login to save trips to your wishlist.</p> | |
| 4719 | - <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">Login</a> | |
| 4720 | - <button type="button" class="yatra-btn-secondary" onclick="this.closest('.yatra-enquiry-modal').classList.remove('active'); document.body.style.overflow = '';">Cancel</button> | |
| 4979 | + <h3 style="margin-bottom: 20px;">${loginRequiredHeading}</h3> | |
| 4980 | + <p style="margin-bottom: 30px;">${loginRequiredBody}</p> | |
| 4981 | + <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">${loginButtonLabel}</a> | |
| 4982 | + <button type="button" class="yatra-btn-secondary" onclick="this.closest('.yatra-enquiry-modal').classList.remove('active'); document.body.style.overflow = '';">${cancelButtonLabel}</button> | |
| 4721 | 4983 | `; |
| 4722 | 4984 | modalContent.innerHTML = ''; |
| 4723 | 4985 | modalContent.appendChild(loginMessage); |
| 4724 | 4986 | } |
| @@ -4723,9 +4985,9 @@ | ||
| 4723 | 4985 | modalContent.appendChild(loginMessage); |
| 4724 | 4986 | } |
| 4725 | 4987 | } else { |
| 4726 | 4988 | // Fallback: show alert and redirect to login |
| 4727 | - if (confirm('Please login to save trips to your wishlist. Would you like to go to the login page?')) { | |
| 4989 | + if (confirm(__('Please login to save trips to your wishlist. Would you like to go to the login page?', 'yatra'))) { | |
| 4728 | 4990 | window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php'; |
| 4729 | 4991 | } |
| 4730 | 4992 | } |
| 4731 | 4993 | } |
| @@ -4975,15 +5237,15 @@ | ||
| 4975 | 5237 | const videoUrl = link.getAttribute('href'); |
| 4976 | 5238 | if (!videoUrl || videoUrl === '#') return; |
| 4977 | 5239 | |
| 4978 | 5240 | const img = overlay.closest('.yatra-media-card').querySelector('img'); |
| 4979 | - const videoTitle = img ? img.alt : 'Itinerary Video'; | |
| 5241 | + const videoTitle = img ? img.alt : __('Itinerary Video', 'yatra'); | |
| 4980 | 5242 | |
| 4981 | 5243 | // Extract video ID from URL |
| 4982 | 5244 | let videoId = ''; |
| 4983 | 5245 | if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { |
| 4984 | 5246 | // YouTube video ID extraction |
| 4985 | - const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/); | |
| 5247 | + const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([^&\n?#\/]+)/); | |
| 4986 | 5248 | videoId = matches ? matches[1] : ''; |
| 4987 | 5249 | } else if (videoUrl.includes('vimeo.com')) { |
| 4988 | 5250 | // Vimeo video ID extraction |
| 4989 | 5251 | const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/); |
| @@ -5096,9 +5358,9 @@ | ||
| 5096 | 5358 | try { |
| 5097 | 5359 | // Get download URL with signature using centralized helper |
| 5098 | 5360 | const response = await window.YatraApiHelper.getDownloadInfo(downloadId); |
| 5099 | 5361 | if (!response.ok) { |
| 5100 | - throw new Error('Download failed'); | |
| 5362 | + throw new Error(__('Download failed', 'yatra')); | |
| 5101 | 5363 | } |
| 5102 | 5364 | |
| 5103 | 5365 | const data = await response.json(); |
| 5104 | 5366 | |
| @@ -5115,9 +5377,9 @@ | ||
| 5115 | 5377 | } |
| 5116 | 5378 | } catch (error) { |
| 5117 | 5379 | console.error('Download error:', error); |
| 5118 | 5380 | // Show error message to user |
| 5119 | - this.showNotification('Download failed. Please try again.', 'error'); | |
| 5381 | + this.showNotification(__('Download failed. Please try again.', 'yatra'), 'error'); | |
| 5120 | 5382 | } |
| 5121 | 5383 | } |
| 5122 | 5384 | |
| 5123 | 5385 | async previewFile(downloadId) { |
| @@ -5127,15 +5389,15 @@ | ||
| 5127 | 5389 | |
| 5128 | 5390 | // Get download info using centralized helper |
| 5129 | 5391 | const response = await window.YatraApiHelper.getDownloadInfo(downloadId); |
| 5130 | 5392 | if (!response.ok) { |
| 5131 | - throw new Error('Failed to get file info'); | |
| 5393 | + throw new Error(__('Failed to get file info', 'yatra')); | |
| 5132 | 5394 | } |
| 5133 | 5395 | |
| 5134 | 5396 | const data = await response.json(); |
| 5135 | 5397 | |
| 5136 | 5398 | if (data.download_url) { |
| 5137 | - this.loadPreview(data.download_url, data.title || 'Preview'); | |
| 5399 | + this.loadPreview(data.download_url, data.title || __('Preview', 'yatra')); | |
| 5138 | 5400 | } else { |
| 5139 | 5401 | throw new Error('File URL not found'); |
| 5140 | 5402 | } |
| 5141 | 5403 | } catch (error) { |
| @@ -5140,9 +5402,9 @@ | ||
| 5140 | 5402 | } |
| 5141 | 5403 | } catch (error) { |
| 5142 | 5404 | console.error('Preview error:', error); |
| 5143 | 5405 | this.closePreviewModal(); |
| 5144 | - this.showNotification('Failed to load preview. Please try again.', 'error'); | |
| 5406 | + this.showNotification(__('Failed to load preview. Please try again.', 'yatra'), 'error'); | |
| 5145 | 5407 | } |
| 5146 | 5408 | } |
| 5147 | 5409 | |
| 5148 | 5410 | loadPreview(fileUrl, title) { |
| @@ -5170,13 +5432,15 @@ | ||
| 5170 | 5432 | previewContent.innerHTML = ` |
| 5171 | 5433 | <iframe src="${fileUrl}" style="width: 100%; height: 500px; border: none; border-radius: 8px;"></iframe> |
| 5172 | 5434 | `; |
| 5173 | 5435 | } else { |
| 5436 | + const previewUnavailableTitle = __('Preview not available for this file type', 'yatra'); | |
| 5437 | + const previewUnavailableHint = __('Please download the file to view its contents', 'yatra'); | |
| 5174 | 5438 | previewContent.innerHTML = ` |
| 5175 | 5439 | <div style="text-align: center; padding: 40px;"> |
| 5176 | 5440 | <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">📄</div> |
| 5177 | - <p style="color: #6b7280; margin-bottom: 16px;">Preview not available for this file type</p> | |
| 5178 | - <p style="color: #9ca3af; font-size: 14px;">Please download the file to view its contents</p> | |
| 5441 | + <p style="color: #6b7280; margin-bottom: 16px;">${previewUnavailableTitle}</p> | |
| 5442 | + <p style="color: #9ca3af; font-size: 14px;">${previewUnavailableHint}</p> | |
| 5179 | 5443 | </div> |
| 5180 | 5444 | `; |
| 5181 | 5445 | } |
| 5182 | 5446 | } |
| @@ -5198,12 +5462,13 @@ | ||
| 5198 | 5462 | |
| 5199 | 5463 | // Reset loading state |
| 5200 | 5464 | const previewContent = document.getElementById('preview-content'); |
| 5201 | 5465 | if (previewContent) { |
| 5466 | + const loadingPreviewLabel = __('Loading preview...', 'yatra'); | |
| 5202 | 5467 | previewContent.innerHTML = ` |
| 5203 | 5468 | <div class="yatra-preview-loading"> |
| 5204 | 5469 | <div class="yatra-preview-spinner"></div> |
| 5205 | - <p>Loading preview...</p> | |
| 5470 | + <p>${loadingPreviewLabel}</p> | |
| 5206 | 5471 | </div> |
| 5207 | 5472 | `; |
| 5208 | 5473 | } |
| 5209 | 5474 | } |