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 | assets/js/single-trip.js +171 -20 3.0.4 → 3.0.16 View file →
@@ -9,8 +9,49 @@
9 9 *
10 10 * @package Yatra
11 11 */
12 12
13 +// Translation helpers — idempotent shim matching booking.js / trip.js so
14 +// `__()` resolves through wp.i18n when wp-i18n is enqueued, otherwise it
15 +// falls back to the source string.
16 +// Parse a numeric min/max attribute while preserving a legitimate 0.
17 +// `parseInt(el.getAttribute('max')) || 99` treated max="0" — what a sold-out
18 +// departure renders — as "unset" and fell back to the placeholder ceiling, so
19 +// the guest selector stayed usable and ignored the departure's real capacity.
20 +// Only an absent or non-numeric value may fall back. Installed once and shared
21 +// by every frontend script, so load order does not matter.
22 +if (typeof window.yatraNumOr !== 'function') {
23 + window.yatraNumOr = function (raw, fallback) {
24 + var parsed = parseInt(raw, 10);
25 + return isNaN(parsed) ? fallback : parsed;
26 + };
27 +}
28 +
29 +(function () {
30 + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
31 + if (i18n && typeof i18n.__ === 'function') {
32 + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); };
33 + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); };
34 + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); };
35 + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s).
36 + // Do NOT defer to a pre-existing window.sprintf: a theme or other
37 + // plugin can define an incompatible global that leaves placeholders
38 + // unsubstituted (the cause of literal "%d"/"%s" in the UI).
39 + if (typeof i18n.sprintf === 'function') {
40 + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); };
41 + } else if (typeof window.sprintf !== 'function') {
42 + window.sprintf = function (fmt) { return fmt; };
43 + }
44 + } else {
45 + // wp.i18n not present — install passthroughs only if nothing usable
46 + // exists yet (don't clobber a real binding installed earlier).
47 + if (typeof window.__ !== 'function') window.__ = function (text) { return text; };
48 + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; };
49 + if (typeof window._x !== 'function') window._x = function (text) { return text; };
50 + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; };
51 + }
52 +})();
53 +
13 54 document.addEventListener('DOMContentLoaded', function() {
14 55 function yatraGetFlatpickrLocale() {
15 56 var l = window.yatraTripData && window.yatraTripData.flatpickrLocale;
16 57 if (!l || typeof l !== 'object') {
@@ -22,9 +63,72 @@
22 63 }
23 64 return out;
24 65 }
25 66
67 + /**
68 + * Convert a PHP date() format string to its Flatpickr equivalent.
69 + *
70 + * The two format spec tables overlap heavily — d/j/D/l/m/n/M/F/Y/y
71 + * mean the same thing in both — but a few PHP tokens map to a
72 + * different Flatpickr token (e.g. PHP `s` = seconds → Flatpickr
73 + * `S`; PHP `a`/`A` AM-PM → Flatpickr `K`). Anything outside the
74 + * map (literal punctuation, slashes, spaces) is passed through.
75 + *
76 + * Used to populate flatpickr's `altFormat`. We keep `dateFormat`
77 + * itself locked to `Y-m-d` so the value submitted to the backend
78 + * stays stable regardless of the operator's display preference;
79 + * only the user-visible input swaps to their configured format.
80 + *
81 + * Kept in lockstep with the helper of the same shape in trip.js
82 + * and the inline mapping in single-trip-sidebar.js — three call
83 + * sites because each file ships as its own enqueued bundle and
84 + * sharing across them would require a separate dep file.
85 + *
86 + * @param {string} php PHP date() format from Settings → General → Date format
87 + * @returns {string} Flatpickr-compatible format string
88 + */
89 + function yatraPhpDateFormatToFlatpickr(php) {
90 + if (!php || typeof php !== 'string') return 'F j, Y';
91 + var map = {
92 + // Day
93 + d: 'd', j: 'j', D: 'D', l: 'l',
94 + // Month
95 + m: 'm', n: 'n', M: 'M', F: 'F',
96 + // Year
97 + Y: 'Y', y: 'y',
98 + // Time
99 + H: 'H', G: 'H', h: 'h', g: 'h',
100 + i: 'i', s: 'S', a: 'K', A: 'K'
101 + };
102 + var out = '';
103 + var esc = false;
104 + for (var i = 0; i < php.length; i++) {
105 + var ch = php[i];
106 + if (esc) { out += ch; esc = false; continue; }
107 + if (ch === '\\') { esc = true; continue; }
108 + out += map[ch] || ch;
109 + }
110 + return out || 'F j, Y';
111 + }
112 +
113 + /**
114 + * Resolve the operator's configured display format and convert it
115 + * for Flatpickr. Falls back to `F j, Y` (e.g. "Jan 5, 2026") when
116 + * settings haven't shipped a value.
117 + */
118 + function yatraResolveAltFormat() {
119 + var cfg = window.yatraTripData || {};
120 + // Pre-converted value wins (set server-side in some bundles); a
121 + // raw PHP string is converted on the fly.
122 + if (cfg.flatpickrAltFormat || cfg.altDateFormat) {
123 + return cfg.flatpickrAltFormat || cfg.altDateFormat;
124 + }
125 + var php = cfg.dateFormat || cfg.date_format || '';
126 + return yatraPhpDateFormatToFlatpickr(php);
127 + }
128 +
26 129 var fpLocale = yatraGetFlatpickrLocale();
130 + var fpAltFormat = yatraResolveAltFormat();
27 131
28 132 // Initialize date pickers when DOM is ready.
29 133 //
30 134 // Priority order applied here MUST mirror the server-side
@@ -61,9 +165,16 @@
61 165 : [];
62 166
63 167 if (availabilityDates.length > 0) {
64 168 flatpickr(bookingDateInput, {
169 + // Backend value stays as Y-m-d (what the booking
170 + // form submits); only the user-visible input flips
171 + // to the operator's configured Date Format via
172 + // altInput/altFormat. Previously hardcoded which
173 + // ignored Settings → General → Date format.
65 174 dateFormat: 'Y-m-d',
175 + altInput: true,
176 + altFormat: fpAltFormat,
66 177 minDate: 'today',
67 178 enable: availabilityDates,
68 179 disableMobile: false,
69 180 locale: fpLocale
@@ -73,9 +184,16 @@
73 184 // the merged date list is empty (e.g. all rules expired).
74 185 // Disable the input rather than silently falling back to
75 186 // "any date", which would violate the configured priority.
76 187 bookingDateInput.setAttribute('disabled', 'disabled');
77 - bookingDateInput.setAttribute('placeholder', bookingDateInput.getAttribute('data-empty-text') || 'No departure dates available');
188 + // Fallback placeholder when the template hasn't supplied
189 + // `data-empty-text`. Wrapped in __() so the literal string
190 + // can be translated; the data-attribute fallback is a
191 + // server-rendered string that's already localised.
192 + bookingDateInput.setAttribute(
193 + 'placeholder',
194 + bookingDateInput.getAttribute('data-empty-text') || __('No departure dates available', 'yatra')
195 + );
78 196 }
79 197 } else {
80 198 // Flexible booking: any future date inside the trip's available
81 199 // window (available_from / available_to are surfaced as
@@ -82,10 +200,15 @@
82 200 // data-min-date / data-max-date by the sidebar template).
83 201 const minDate = bookingDateInput.getAttribute('data-min-date') || 'today';
84 202 const maxDate = bookingDateInput.getAttribute('data-max-date');
85 203
204 + // Flexible-date picker. Same altInput/altFormat pattern
205 + // as the availability-restricted branch above — backend
206 + // value stable, display matches operator preference.
86 207 const config = {
87 208 dateFormat: 'Y-m-d',
209 + altInput: true,
210 + altFormat: fpAltFormat,
88 211 minDate: minDate,
89 212 disableMobile: false,
90 213 locale: fpLocale
91 214 };
@@ -96,14 +219,19 @@
96 219
97 220 flatpickr(bookingDateInput, config);
98 221 }
99 222 }
100 -
101 - // Enquiry modal date picker (always flexible)
223 +
224 + // Enquiry modal date picker (always flexible). Same altFormat
225 + // treatment so the enquiry form respects the operator's date
226 + // format too — previously hardcoded to Y-m-d which jarred with
227 + // any non-ISO display preference.
102 228 const enquiryDateInput = document.getElementById('enquiry-travel-date');
103 229 if (enquiryDateInput) {
104 230 flatpickr(enquiryDateInput, {
105 231 dateFormat: 'Y-m-d',
232 + altInput: true,
233 + altFormat: fpAltFormat,
106 234 minDate: 'today',
107 235 disableMobile: false,
108 236 locale: fpLocale
109 237 });
@@ -246,10 +374,16 @@
246 374
247 375 updatePricing();
248 376 });
249 377
250 - // Set today's date by default if available, otherwise use trip fallback pricing
251 - const today = new Date().toISOString().split('T')[0];
378 + // Set today's date by default if available, otherwise use trip fallback pricing.
379 + // Build LOCAL YYYY-MM-DD, not toISOString() (which is UTC and can land on
380 + // the wrong calendar day — mis-matching the availability's departure_date
381 + // and pre-selecting the wrong date near midnight).
382 + const _now = new Date();
383 + const today = _now.getFullYear() + '-'
384 + + String(_now.getMonth() + 1).padStart(2, '0') + '-'
385 + + String(_now.getDate()).padStart(2, '0');
252 386 if (!dateInput.value) {
253 387 const availabilities = getSidebarAvailabilities();
254 388 if (availabilities.length > 0) {
255 389 const todayAvailability = availabilities.find(a => a.departure_date === today);
@@ -277,13 +411,18 @@
277 411 categoryInputs.forEach(input => {
278 412 const count = parseInt(input.value) || 0;
279 413 const price = parseFloat(input.dataset.price) || 0;
280 414 const pricingMode = input.dataset.pricingMode || 'per_person';
281 -
415 + const groupOverflow = input.dataset.groupOverflow || 'block';
416 + const maxPax = parseInt(input.dataset.maxPax) || 0;
417 +
282 418 if (pricingMode === 'per_group') {
283 - // Per group: charge flat price once if any travelers
419 + // Per group: one flat price, or per block of maxPax people
420 + // when overflow is "per_block".
284 421 if (count > 0) {
285 - subtotal += price;
422 + subtotal += (groupOverflow === 'per_block' && maxPax > 0)
423 + ? price * Math.ceil(count / maxPax)
424 + : price;
286 425 }
287 426 } else {
288 427 // Per person: charge per traveler
289 428 subtotal += price * count;
@@ -331,9 +470,9 @@
331 470 <div class="yatra-group-discount-badge">
332 471 <svg width="14" height="14" fill="currentColor" viewBox="0 0 20 20">
333 472 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
334 473 </svg>
335 - Group discount applied: -${window.yatraTripData?.currencySymbol || '$'}${discountAmount.toFixed(2)}
474 + Group discount applied: -${typeof window.yatra_format_price_js === 'function' ? window.yatra_format_price_js(discountAmount) : (window.yatraTripData?.currencySymbol || '$') + discountAmount.toFixed(2)}
336 475 </div>
337 476 `;
338 477 } else if (indicator) {
339 478 indicator.remove();
@@ -374,12 +513,19 @@
374 513 // Traveler-based pricing - set adults to group size
375 514 const adultInput = document.querySelector('input[name*="travelers[adults]"]');
376 515 if (adultInput) {
377 516 adultInput.value = groupSize;
378 - // Update display
517 + // Update display. The literal "Adult x %d"
518 + // is kept as the msgid so translators can
519 + // localise both the noun and the format
520 + // (some locales put the multiplier first).
521 + // Comment phrasing matches the other call
522 + // sites in trip.js so WP-CLI sees ONE
523 + // canonical translators-comment per msgid.
379 524 const display = document.querySelector('.yatra-participants-display');
380 525 if (display) {
381 - display.textContent = `Adult x ${groupSize}`;
526 + // translators: %d: number of adults
527 + display.textContent = __('Adult x %d', 'yatra').replace('%d', String(groupSize));
382 528 }
383 529 }
384 530 }
385 531
@@ -426,10 +572,10 @@
426 572 }
427 573
428 574 if (!input) return;
429 575
430 - const min = parseInt(input.getAttribute('min')) || 0;
431 - const max = parseInt(input.getAttribute('max')) || 99;
576 + const min = window.yatraNumOr(input.getAttribute('min'), 0);
577 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
432 578 let value = parseInt(input.value) || 0;
433 579
434 580 // Update value based on button type
435 581 if (btn.classList.contains('yatra-quantity-plus')) {
@@ -485,9 +631,9 @@
485 631 // Regular pricing - sync all num_travelers and enquiry_adults inputs
486 632 const regularInputs = document.querySelectorAll('#num_travelers, #enquiry_adults');
487 633 regularInputs.forEach(input => {
488 634 if (input !== sourceInput) {
489 - const max = parseInt(input.getAttribute('max')) || 99;
635 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
490 636 input.value = Math.min(sourceValue, max);
491 637 }
492 638 });
493 639
@@ -494,9 +640,9 @@
494 640 // Also sync to availability section inputs
495 641 const availabilityInputs = document.querySelectorAll('input[id^="num-travelers-"]');
496 642 availabilityInputs.forEach(input => {
497 643 if (input !== sourceInput) {
498 - const max = parseInt(input.getAttribute('max')) || 99;
644 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
499 645 input.value = Math.min(sourceValue, max);
500 646 }
501 647 });
502 648
@@ -521,15 +667,15 @@
521 667 );
522 668
523 669 categoryInputs.forEach(input => {
524 670 if (input !== sourceInput) {
525 - const max = parseInt(input.getAttribute('max')) || 99;
671 + const max = window.yatraNumOr(input.getAttribute('max'), 99);
526 672 input.value = Math.min(sourceValue, max);
527 673
528 674 // Update button states for this input
529 675 const row = input.closest('.yatra-quantity-row');
530 676 if (row) {
531 - const min = parseInt(input.getAttribute('min')) || 0;
677 + const min = window.yatraNumOr(input.getAttribute('min'), 0);
532 678 const minusBtn = row.querySelector('.yatra-quantity-minus');
533 679 const plusBtn = row.querySelector('.yatra-quantity-plus');
534 680 if (minusBtn) minusBtn.disabled = input.value <= min;
535 681 if (plusBtn) plusBtn.disabled = input.value >= max;
@@ -563,10 +709,15 @@
563 709
564 710 inputs.forEach(input => {
565 711 const value = parseInt(input.value) || 0;
566 712 if (value > 0) {
567 - const label = input.getAttribute('data-category-label') || 'Traveler';
568 - parts.push(`${label} x ${value}`);
713 + const label = input.getAttribute('data-category-label') || __('Traveler', 'yatra');
714 + parts.push(sprintf(
715 + /* translators: 1: traveler category label, 2: count. */
716 + __('%1$s x %2$d', 'yatra'),
717 + label,
718 + value
719 + ));
569 720 total += value;
570 721 }
571 722 });
572 723
@@ -572,9 +723,9 @@
572 723
573 724 if (parts.length > 0) {
574 725 display.textContent = parts.join(', ');
575 726 } else {
576 - display.textContent = 'Select travelers';
727 + display.textContent = __('Select travelers', 'yatra');
577 728 }
578 729 });
579 730 }
580 731 });