| @@ -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 | /** |
| @@ -165,9 +205,14 @@ | ||
| 165 | 205 | const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.'; |
| 166 | 206 | |
| 167 | 207 | const num = Number(amount) || 0; |
| 168 | 208 | |
| 169 | - 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', { | |
| 170 | 215 | minimumFractionDigits: decimals, |
| 171 | 216 | maximumFractionDigits: decimals, |
| 172 | 217 | }).format(num) |
| 173 | 218 | .replace(/,/g, 'TEMP_THOUSAND') |
| @@ -293,9 +338,17 @@ | ||
| 293 | 338 | const mediaData = JSON.parse(mediaScript.textContent || '{}'); |
| 294 | 339 | const imageUrls = Array.isArray(mediaData.images) ? mediaData.images : []; |
| 295 | 340 | imageUrls.forEach((url, index) => { |
| 296 | 341 | if (url && typeof url === 'string' && !this.images.some(i => i.src === url)) { |
| 297 | - 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 | + }); | |
| 298 | 351 | } |
| 299 | 352 | }); |
| 300 | 353 | } catch (e) { |
| 301 | 354 | // JSON parse error – fall through to DOM-based collection. |
| @@ -303,23 +356,24 @@ | ||
| 303 | 356 | } |
| 304 | 357 | |
| 305 | 358 | // 2. Collect from hero slides (covers any images not in the JSON blob). |
| 306 | 359 | document.querySelectorAll('.yatra-trip-hero-slide img').forEach((img, index) => { |
| 307 | - 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))); | |
| 308 | 362 | }); |
| 309 | 363 | |
| 310 | 364 | // 3. Collect from gallery section (hero gallery only, NOT itinerary). |
| 311 | 365 | document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item img').forEach((img) => { |
| 312 | - add(img, 'Gallery Image'); | |
| 366 | + add(img, __('Gallery Image', 'yatra')); | |
| 313 | 367 | }); |
| 314 | 368 | |
| 315 | 369 | // 4. Collect from hero main image. |
| 316 | 370 | const heroMainImg = document.querySelector('.yatra-hero-main-img'); |
| 317 | - if (heroMainImg) add(heroMainImg, 'Main Image'); | |
| 371 | + if (heroMainImg) add(heroMainImg, __('Main Image', 'yatra')); | |
| 318 | 372 | |
| 319 | 373 | // 5. Collect from side images. |
| 320 | 374 | document.querySelectorAll('.yatra-side-image-item img').forEach((img) => { |
| 321 | - add(img, 'Side Image'); | |
| 375 | + add(img, __('Side Image', 'yatra')); | |
| 322 | 376 | }); |
| 323 | 377 | } |
| 324 | 378 | |
| 325 | 379 | createThumbnails() { |
| @@ -566,14 +620,22 @@ | ||
| 566 | 620 | }; |
| 567 | 621 | |
| 568 | 622 | // Visible gallery thumbnails inside the scope |
| 569 | 623 | root.querySelectorAll('.yatra-media-card img').forEach((img) => { |
| 570 | - add(img, `Itinerary Gallery Image ${images.length + 1}`); | |
| 624 | + add(img, sprintf( | |
| 625 | + /* translators: %d: gallery image sequence number. */ | |
| 626 | + __('Itinerary Gallery Image %d', 'yatra'), | |
| 627 | + images.length + 1 | |
| 628 | + )); | |
| 571 | 629 | }); |
| 572 | 630 | |
| 573 | 631 | // Video links inside the scope (fallback selector) |
| 574 | 632 | root.querySelectorAll('.yatra-itinerary-video-link img').forEach((img) => { |
| 575 | - add(img, `Itinerary Video ${images.length + 1}`); | |
| 633 | + add(img, sprintf( | |
| 634 | + /* translators: %d: itinerary video sequence number. */ | |
| 635 | + __('Itinerary Video %d', 'yatra'), | |
| 636 | + images.length + 1 | |
| 637 | + )); | |
| 576 | 638 | }); |
| 577 | 639 | |
| 578 | 640 | // If still empty AND we were given no scope, fall back to the hidden global pool |
| 579 | 641 | // so legacy click handlers on `.yatra-itinerary-trip-gallery .yatra-gallery-item` |
| @@ -579,9 +641,13 @@ | ||
| 579 | 641 | // so legacy click handlers on `.yatra-itinerary-trip-gallery .yatra-gallery-item` |
| 580 | 642 | // continue to function. |
| 581 | 643 | if (images.length === 0 && !scope) { |
| 582 | 644 | document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => { |
| 583 | - add(img, `Itinerary Image ${images.length + 1}`); | |
| 645 | + add(img, sprintf( | |
| 646 | + /* translators: %d: itinerary image sequence number. */ | |
| 647 | + __('Itinerary Image %d', 'yatra'), | |
| 648 | + images.length + 1 | |
| 649 | + )); | |
| 584 | 650 | }); |
| 585 | 651 | } |
| 586 | 652 | |
| 587 | 653 | return images; |
| @@ -920,9 +986,9 @@ | ||
| 920 | 986 | } |
| 921 | 987 | |
| 922 | 988 | if (images.length > 0) { |
| 923 | 989 | img.src = images[0]; |
| 924 | - img.alt = 'Trip Image 1'; | |
| 990 | + img.alt = __('Trip Image 1', 'yatra'); | |
| 925 | 991 | if (link) { |
| 926 | 992 | link.href = '#'; |
| 927 | 993 | link.dataset.imageIndex = '0'; |
| 928 | 994 | } |
| @@ -1140,8 +1206,12 @@ | ||
| 1140 | 1206 | if (existingOverlay) existingOverlay.remove(); |
| 1141 | 1207 | |
| 1142 | 1208 | const overlay = document.createElement('div'); |
| 1143 | 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'); | |
| 1144 | 1214 | overlay.innerHTML = ` |
| 1145 | 1215 | <div class="yatra-tour-info"> |
| 1146 | 1216 | <div class="yatra-tour-icon"> |
| 1147 | 1217 | <svg width="80" height="80" viewBox="0 0 100 100" fill="none"> |
| @@ -1154,11 +1224,11 @@ | ||
| 1154 | 1224 | <text x="50" y="58" text-anchor="middle" fill="white" font-size="24" font-weight="bold">360°</text> |
| 1155 | 1225 | </svg> |
| 1156 | 1226 | </div> |
| 1157 | 1227 | <div class="yatra-tour-details"> |
| 1158 | - <div class="yatra-tour-title">${tour.title || '360° Virtual Tour'}</div> | |
| 1159 | - <div class="yatra-tour-type">${tour.tour_type || '360°'} Experience</div> | |
| 1160 | - <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> | |
| 1161 | 1231 | </div> |
| 1162 | 1232 | </div> |
| 1163 | 1233 | `; |
| 1164 | 1234 | |
| @@ -1196,8 +1266,9 @@ | ||
| 1196 | 1266 | if (existingOverlay) existingOverlay.remove(); |
| 1197 | 1267 | |
| 1198 | 1268 | const overlay = document.createElement('div'); |
| 1199 | 1269 | overlay.className = 'yatra-media-overlay yatra-media-document-overlay'; |
| 1270 | + const documentFallbackTitle = __('Document', 'yatra'); | |
| 1200 | 1271 | overlay.innerHTML = ` |
| 1201 | 1272 | <div class="yatra-document-info"> |
| 1202 | 1273 | <div class="yatra-document-icon"> |
| 1203 | 1274 | <svg width="48" height="48" viewBox="0 0 24 24" fill="none"> |
| @@ -1207,9 +1278,9 @@ | ||
| 1207 | 1278 | <path d="M16 17H8" stroke="#374151" stroke-width="2"/> |
| 1208 | 1279 | </svg> |
| 1209 | 1280 | </div> |
| 1210 | 1281 | <div class="yatra-document-details"> |
| 1211 | - <div class="yatra-document-title">${doc.title || 'Document'}</div> | |
| 1282 | + <div class="yatra-document-title">${doc.title || documentFallbackTitle}</div> | |
| 1212 | 1283 | <div class="yatra-document-size">${this.formatFileSize(doc.file_size || 0)}</div> |
| 1213 | 1284 | </div> |
| 1214 | 1285 | </div> |
| 1215 | 1286 | `; |
| @@ -1216,11 +1287,11 @@ | ||
| 1216 | 1287 | container.appendChild(overlay); |
| 1217 | 1288 | } |
| 1218 | 1289 | |
| 1219 | 1290 | formatFileSize(bytes) { |
| 1220 | - if (bytes === 0) return '0 Bytes'; | |
| 1291 | + if (bytes === 0) return '0 ' + __('Bytes', 'yatra'); | |
| 1221 | 1292 | const k = 1024; |
| 1222 | - const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| 1293 | + const sizes = [__('Bytes', 'yatra'), 'KB', 'MB', 'GB']; | |
| 1223 | 1294 | const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 1224 | 1295 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| 1225 | 1296 | } |
| 1226 | 1297 | |
| @@ -1250,9 +1321,10 @@ | ||
| 1250 | 1321 | sideImages.forEach((item, index) => { |
| 1251 | 1322 | const img = item.querySelector('img'); |
| 1252 | 1323 | if (img && images[index + 1]) { |
| 1253 | 1324 | img.src = images[index + 1]; |
| 1254 | - 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); | |
| 1255 | 1327 | } |
| 1256 | 1328 | }); |
| 1257 | 1329 | } |
| 1258 | 1330 | |
| @@ -1257,8 +1329,9 @@ | ||
| 1257 | 1329 | } |
| 1258 | 1330 | |
| 1259 | 1331 | updateSideVideos(sideImages) { |
| 1260 | 1332 | const videos = this.mediaData.videos || []; |
| 1333 | + const fallbackVideoTitle = __('Trip Video', 'yatra'); | |
| 1261 | 1334 | sideImages.forEach((item, index) => { |
| 1262 | 1335 | const img = item.querySelector('img'); |
| 1263 | 1336 | if (img && videos[index + 1]) { |
| 1264 | 1337 | const video = videos[index + 1]; |
| @@ -1264,9 +1337,9 @@ | ||
| 1264 | 1337 | const video = videos[index + 1]; |
| 1265 | 1338 | if (video.thumbnail) { |
| 1266 | 1339 | img.src = video.thumbnail; |
| 1267 | 1340 | } |
| 1268 | - img.alt = video.title || 'Trip Video'; | |
| 1341 | + img.alt = video.title || fallbackVideoTitle; | |
| 1269 | 1342 | } |
| 1270 | 1343 | }); |
| 1271 | 1344 | } |
| 1272 | 1345 | |
| @@ -1271,8 +1344,9 @@ | ||
| 1271 | 1344 | } |
| 1272 | 1345 | |
| 1273 | 1346 | updateSideYoutubeVideos(sideImages) { |
| 1274 | 1347 | const youtubeVideos = this.mediaData.youtube_videos || []; |
| 1348 | + const fallbackYoutubeTitle = __('YouTube Video', 'yatra'); | |
| 1275 | 1349 | sideImages.forEach((item, index) => { |
| 1276 | 1350 | const img = item.querySelector('img'); |
| 1277 | 1351 | if (img && youtubeVideos[index + 1]) { |
| 1278 | 1352 | const video = youtubeVideos[index + 1]; |
| @@ -1278,9 +1352,9 @@ | ||
| 1278 | 1352 | const video = youtubeVideos[index + 1]; |
| 1279 | 1353 | if (video.thumbnail) { |
| 1280 | 1354 | img.src = video.thumbnail; |
| 1281 | 1355 | } |
| 1282 | - img.alt = video.title || 'YouTube Video'; | |
| 1356 | + img.alt = video.title || fallbackYoutubeTitle; | |
| 1283 | 1357 | } |
| 1284 | 1358 | }); |
| 1285 | 1359 | } |
| 1286 | 1360 | |
| @@ -1285,8 +1359,9 @@ | ||
| 1285 | 1359 | } |
| 1286 | 1360 | |
| 1287 | 1361 | updateSideTours(sideImages) { |
| 1288 | 1362 | const virtualTours = this.mediaData.virtual_tours || []; |
| 1363 | + const fallbackTourTitle = __('360° Virtual Tour', 'yatra'); | |
| 1289 | 1364 | sideImages.forEach((item, index) => { |
| 1290 | 1365 | const img = item.querySelector('img'); |
| 1291 | 1366 | if (img && virtualTours[index + 1]) { |
| 1292 | 1367 | const tour = virtualTours[index + 1]; |
| @@ -1292,9 +1367,9 @@ | ||
| 1292 | 1367 | const tour = virtualTours[index + 1]; |
| 1293 | 1368 | if (tour.thumbnail) { |
| 1294 | 1369 | img.src = tour.thumbnail; |
| 1295 | 1370 | } |
| 1296 | - img.alt = tour.title || '360° Virtual Tour'; | |
| 1371 | + img.alt = tour.title || fallbackTourTitle; | |
| 1297 | 1372 | } |
| 1298 | 1373 | }); |
| 1299 | 1374 | } |
| 1300 | 1375 | |
| @@ -1299,14 +1374,15 @@ | ||
| 1299 | 1374 | } |
| 1300 | 1375 | |
| 1301 | 1376 | updateSideDocuments(sideImages) { |
| 1302 | 1377 | const documents = this.mediaData.documents || []; |
| 1378 | + const fallbackDocumentTitle = __('Trip Document', 'yatra'); | |
| 1303 | 1379 | sideImages.forEach((item, index) => { |
| 1304 | 1380 | const img = item.querySelector('img'); |
| 1305 | 1381 | if (img && documents[index + 1]) { |
| 1306 | 1382 | const doc = documents[index + 1]; |
| 1307 | 1383 | // Keep existing image for documents |
| 1308 | - img.alt = doc.title || 'Trip Document'; | |
| 1384 | + img.alt = doc.title || fallbackDocumentTitle; | |
| 1309 | 1385 | } |
| 1310 | 1386 | }); |
| 1311 | 1387 | } |
| 1312 | 1388 | |
| @@ -1533,10 +1609,10 @@ | ||
| 1533 | 1609 | if (!minusBtn || !plusBtn) return; |
| 1534 | 1610 | |
| 1535 | 1611 | const updateButtonStates = () => { |
| 1536 | 1612 | const current = parseInt(input.value) || 1; |
| 1537 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 1538 | - 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); | |
| 1539 | 1615 | |
| 1540 | 1616 | minusBtn.disabled = current <= min; |
| 1541 | 1617 | plusBtn.disabled = current >= max; |
| 1542 | 1618 | }; |
| @@ -1546,9 +1622,9 @@ | ||
| 1546 | 1622 | e.preventDefault(); |
| 1547 | 1623 | e.stopPropagation(); |
| 1548 | 1624 | |
| 1549 | 1625 | const current = parseInt(input.value) || 1; |
| 1550 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 1626 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 1551 | 1627 | |
| 1552 | 1628 | if (current > min) { |
| 1553 | 1629 | input.value = current - 1; |
| 1554 | 1630 | updateButtonStates(); |
| @@ -1561,9 +1637,9 @@ | ||
| 1561 | 1637 | e.preventDefault(); |
| 1562 | 1638 | e.stopPropagation(); |
| 1563 | 1639 | |
| 1564 | 1640 | const current = parseInt(input.value) || 1; |
| 1565 | - const max = parseInt(input.getAttribute('max')) || 20; | |
| 1641 | + const max = window.yatraNumOr(input.getAttribute('max'), 20); | |
| 1566 | 1642 | |
| 1567 | 1643 | if (current < max) { |
| 1568 | 1644 | input.value = current + 1; |
| 1569 | 1645 | updateButtonStates(); |
| @@ -1637,21 +1713,25 @@ | ||
| 1637 | 1713 | const parts = []; |
| 1638 | 1714 | travelerInputs.forEach((input) => { |
| 1639 | 1715 | const value = parseInt(input.value || 0); |
| 1640 | 1716 | if (value > 0) { |
| 1641 | - const categoryLabel = input.getAttribute('data-category-label') || 'Traveler'; | |
| 1717 | + const categoryLabel = input.getAttribute('data-category-label') || __('Traveler', 'yatra'); | |
| 1642 | 1718 | parts.push(`${categoryLabel} x ${value}`); |
| 1643 | 1719 | } |
| 1644 | 1720 | }); |
| 1645 | 1721 | |
| 1646 | 1722 | if (parts.length === 0) { |
| 1647 | - // 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. | |
| 1648 | 1727 | const firstInput = travelerInputs[0]; |
| 1649 | 1728 | if (firstInput) { |
| 1650 | - const categoryLabel = firstInput.getAttribute('data-category-label') || 'Traveler'; | |
| 1651 | - 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); | |
| 1652 | 1732 | } else { |
| 1653 | - display.textContent = '1 Traveler'; | |
| 1733 | + display.textContent = __('1 Traveler', 'yatra'); | |
| 1654 | 1734 | } |
| 1655 | 1735 | } else { |
| 1656 | 1736 | display.textContent = parts.join(', '); |
| 1657 | 1737 | } |
| @@ -1745,9 +1825,9 @@ | ||
| 1745 | 1825 | categoryInputs.forEach((catInput) => { |
| 1746 | 1826 | const catValue = parseInt(catInput.value) || 0; |
| 1747 | 1827 | if (catValue > 0) { |
| 1748 | 1828 | const catRow = catInput.closest('.yatra-quantity-row'); |
| 1749 | - const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 1829 | + const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 1750 | 1830 | parts.push(`${categoryLabel} x ${catValue}`); |
| 1751 | 1831 | } |
| 1752 | 1832 | }); |
| 1753 | 1833 | if (parts.length > 0) { |
| @@ -1822,14 +1902,14 @@ | ||
| 1822 | 1902 | |
| 1823 | 1903 | |
| 1824 | 1904 | // Basic validation |
| 1825 | 1905 | if (!date) { |
| 1826 | - alert('Please select a travel date'); | |
| 1906 | + alert(__('Please select a travel date', 'yatra')); | |
| 1827 | 1907 | return; |
| 1828 | 1908 | } |
| 1829 | 1909 | |
| 1830 | 1910 | if (totalTravelers === 0) { |
| 1831 | - alert('Please select number of travelers'); | |
| 1911 | + alert(__('Please select number of travelers', 'yatra')); | |
| 1832 | 1912 | return; |
| 1833 | 1913 | } |
| 1834 | 1914 | |
| 1835 | 1915 | // Show loading state |
| @@ -1836,9 +1916,17 @@ | ||
| 1836 | 1916 | if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) { |
| 1837 | 1917 | this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML; |
| 1838 | 1918 | } |
| 1839 | 1919 | if (this.checkAvailabilityBtn) { |
| 1840 | - 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'); | |
| 1841 | 1929 | this.checkAvailabilityBtn.disabled = true; |
| 1842 | 1930 | } |
| 1843 | 1931 | |
| 1844 | 1932 | // Get trip ID from data attribute or global variable |
| @@ -1878,11 +1966,12 @@ | ||
| 1878 | 1966 | if (!response.ok) { |
| 1879 | 1967 | // Try to get error message from response |
| 1880 | 1968 | return response.json().then(err => { |
| 1881 | 1969 | console.error('API Error:', err); |
| 1882 | - throw new Error(err.message || 'Failed to fetch availability'); | |
| 1970 | + throw new Error(err.message || __('Failed to fetch availability', 'yatra')); | |
| 1883 | 1971 | }).catch(() => { |
| 1884 | - 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)); | |
| 1885 | 1974 | }); |
| 1886 | 1975 | } |
| 1887 | 1976 | return response.json(); |
| 1888 | 1977 | }) |
| @@ -1946,9 +2035,9 @@ | ||
| 1946 | 2035 | this.resetButton(); |
| 1947 | 2036 | }) |
| 1948 | 2037 | .catch(error => { |
| 1949 | 2038 | console.error('Error loading availability:', error); |
| 1950 | - alert('Failed to load availability. Please try again.'); | |
| 2039 | + alert(__('Failed to load availability. Please try again.', 'yatra')); | |
| 1951 | 2040 | this.resetButton(); |
| 1952 | 2041 | }); |
| 1953 | 2042 | } |
| 1954 | 2043 | |
| @@ -1997,13 +2086,14 @@ | ||
| 1997 | 2086 | // Create new menu item |
| 1998 | 2087 | const newItem = document.createElement('a'); |
| 1999 | 2088 | newItem.href = '#availability'; |
| 2000 | 2089 | newItem.className = 'yatra-sticky-nav-item'; |
| 2090 | + const availabilityLabel = __('Availability', 'yatra'); | |
| 2001 | 2091 | newItem.innerHTML = ` |
| 2002 | 2092 | <svg class="yatra-icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 2003 | 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"/> |
| 2004 | 2094 | </svg> |
| 2005 | - <span>Availability</span> | |
| 2095 | + <span>${availabilityLabel}</span> | |
| 2006 | 2096 | `; |
| 2007 | 2097 | |
| 2008 | 2098 | // Insert before the target item (or after "What's Included" if no target) |
| 2009 | 2099 | if (insertBeforeItem && insertBeforeItem.parentNode) { |
| @@ -2070,9 +2160,9 @@ | ||
| 2070 | 2160 | if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) { |
| 2071 | 2161 | this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML; |
| 2072 | 2162 | } |
| 2073 | 2163 | if (this.checkAvailabilityBtn) { |
| 2074 | - 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'); | |
| 2075 | 2165 | this.checkAvailabilityBtn.disabled = true; |
| 2076 | 2166 | } |
| 2077 | 2167 | |
| 2078 | 2168 | const availSection = document.getElementById('availability'); |
| @@ -2096,9 +2186,9 @@ | ||
| 2096 | 2186 | credentials: 'omit', |
| 2097 | 2187 | }) |
| 2098 | 2188 | .then(response => { |
| 2099 | 2189 | if (!response.ok) { |
| 2100 | - throw new Error('Failed to fetch availability'); | |
| 2190 | + throw new Error(__('Failed to fetch availability', 'yatra')); | |
| 2101 | 2191 | } |
| 2102 | 2192 | return response.json(); |
| 2103 | 2193 | }) |
| 2104 | 2194 | .then(data => { |
| @@ -2133,9 +2223,9 @@ | ||
| 2133 | 2223 | this.resetButton(); |
| 2134 | 2224 | }) |
| 2135 | 2225 | .catch(error => { |
| 2136 | 2226 | console.error('Error reloading availability:', error); |
| 2137 | - alert('Failed to reload availability. Please try again.'); | |
| 2227 | + alert(__('Failed to reload availability. Please try again.', 'yatra')); | |
| 2138 | 2228 | this.resetButton(); |
| 2139 | 2229 | }); |
| 2140 | 2230 | } |
| 2141 | 2231 | |
| @@ -2233,9 +2323,13 @@ | ||
| 2233 | 2323 | if (data && data.success) { |
| 2234 | 2324 | // Update departures available text |
| 2235 | 2325 | const departuresText = document.querySelector('.yatra-departures-available-text'); |
| 2236 | 2326 | if (departuresText && data.departures_count !== undefined) { |
| 2237 | - 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 | + ); | |
| 2238 | 2332 | } |
| 2239 | 2333 | |
| 2240 | 2334 | // Update traveler dropdown HTML |
| 2241 | 2335 | const quantitySelector = document.getElementById('quantity-selector'); |
| @@ -2289,15 +2383,15 @@ | ||
| 2289 | 2383 | const row = travelerSelector.querySelector('.yatra-quantity-row[data-category-id="' + categoryId + '"]'); |
| 2290 | 2384 | if (row) { |
| 2291 | 2385 | const cardInput = row.querySelector('input[type="number"]'); |
| 2292 | 2386 | if (cardInput) { |
| 2293 | - const max = parseInt(cardInput.getAttribute('max')) || 99; | |
| 2387 | + const max = window.yatraNumOr(cardInput.getAttribute('max'), 99); | |
| 2294 | 2388 | cardInput.value = Math.min(value, max); |
| 2295 | 2389 | |
| 2296 | 2390 | // Update button states |
| 2297 | 2391 | const minusBtn = row.querySelector('.yatra-quantity-minus'); |
| 2298 | 2392 | const plusBtn = row.querySelector('.yatra-quantity-plus'); |
| 2299 | - const min = parseInt(cardInput.getAttribute('min')) || 0; | |
| 2393 | + const min = window.yatraNumOr(cardInput.getAttribute('min'), 0); | |
| 2300 | 2394 | if (minusBtn) minusBtn.disabled = cardInput.value <= min; |
| 2301 | 2395 | if (plusBtn) plusBtn.disabled = cardInput.value >= max; |
| 2302 | 2396 | } |
| 2303 | 2397 | } |
| @@ -2308,9 +2402,9 @@ | ||
| 2308 | 2402 | |
| 2309 | 2403 | } else if (simpleTravelers && numTravelersInput) { |
| 2310 | 2404 | // Simple pricing - sync total count |
| 2311 | 2405 | const totalCount = parseInt(numTravelersInput.value) || 1; |
| 2312 | - const max = parseInt(simpleTravelers.getAttribute('max')) || 99; | |
| 2406 | + const max = window.yatraNumOr(simpleTravelers.getAttribute('max'), 99); | |
| 2313 | 2407 | simpleTravelers.value = Math.min(totalCount, max); |
| 2314 | 2408 | |
| 2315 | 2409 | // Update button states |
| 2316 | 2410 | const controls = simpleTravelers.closest('.yatra-quantity-controls-inline'); |
| @@ -2316,9 +2410,9 @@ | ||
| 2316 | 2410 | const controls = simpleTravelers.closest('.yatra-quantity-controls-inline'); |
| 2317 | 2411 | if (controls) { |
| 2318 | 2412 | const minusBtn = controls.querySelector('.yatra-quantity-minus'); |
| 2319 | 2413 | const plusBtn = controls.querySelector('.yatra-quantity-plus'); |
| 2320 | - const min = parseInt(simpleTravelers.getAttribute('min')) || 1; | |
| 2414 | + const min = window.yatraNumOr(simpleTravelers.getAttribute('min'), 1); | |
| 2321 | 2415 | if (minusBtn) minusBtn.disabled = simpleTravelers.value <= min; |
| 2322 | 2416 | if (plusBtn) plusBtn.disabled = simpleTravelers.value >= max; |
| 2323 | 2417 | } |
| 2324 | 2418 | } |
| @@ -2351,9 +2445,9 @@ | ||
| 2351 | 2445 | } |
| 2352 | 2446 | } |
| 2353 | 2447 | }); |
| 2354 | 2448 | |
| 2355 | - display.textContent = parts.length > 0 ? parts.join(', ') : '0 Travelers'; | |
| 2449 | + display.textContent = parts.length > 0 ? parts.join(', ') : __('0 Travelers', 'yatra'); | |
| 2356 | 2450 | } |
| 2357 | 2451 | |
| 2358 | 2452 | /** |
| 2359 | 2453 | * Update availability card total price |
| @@ -2381,13 +2475,17 @@ | ||
| 2381 | 2475 | const input = row.querySelector('input[type="number"]'); |
| 2382 | 2476 | const price = parseFloat(row.getAttribute('data-price')) || 0; |
| 2383 | 2477 | const count = parseInt(input?.value) || 0; |
| 2384 | 2478 | const pricingMode = row.getAttribute('data-pricing-mode') || (input ? input.getAttribute('data-pricing-mode') : '') || 'per_person'; |
| 2385 | - | |
| 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 | + | |
| 2386 | 2482 | if (pricingMode === 'per_group') { |
| 2387 | - // Per group: charge flat price once if any travelers | |
| 2483 | + // Per group: one flat price, or per block of maxPax when "per_block". | |
| 2388 | 2484 | if (count > 0) { |
| 2389 | - total += price; | |
| 2485 | + total += (groupOverflow === 'per_block' && maxPax > 0) | |
| 2486 | + ? price * Math.ceil(count / maxPax) | |
| 2487 | + : price; | |
| 2390 | 2488 | } |
| 2391 | 2489 | } else { |
| 2392 | 2490 | // Per person: charge per traveler |
| 2393 | 2491 | total += price * count; |
| @@ -2416,9 +2514,10 @@ | ||
| 2416 | 2514 | totalAmountEl.textContent = String(total.toFixed(2)); |
| 2417 | 2515 | } |
| 2418 | 2516 | |
| 2419 | 2517 | if (totalNoteEl) { |
| 2420 | - 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); | |
| 2421 | 2520 | } |
| 2422 | 2521 | |
| 2423 | 2522 | // Update the card participants display |
| 2424 | 2523 | this.updateAvailabilityCardDisplay(itemId); |
| @@ -2619,17 +2718,22 @@ | ||
| 2619 | 2718 | // Update display function |
| 2620 | 2719 | const updateDisplay = () => { |
| 2621 | 2720 | const adults = parseInt(adultsInput.value || 1); |
| 2622 | 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); | |
| 2623 | 2726 | let text = ''; |
| 2624 | 2727 | if (adults > 0 && children > 0) { |
| 2625 | - 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); | |
| 2626 | 2730 | } else if (adults > 0) { |
| 2627 | - text = `Adult x ${adults}`; | |
| 2731 | + text = adultPart; | |
| 2628 | 2732 | } else if (children > 0) { |
| 2629 | - text = `Child x ${children}`; | |
| 2733 | + text = childPart; | |
| 2630 | 2734 | } else { |
| 2631 | - text = 'Adult x 1'; | |
| 2735 | + text = sprintf(_n('Adult x %d', 'Adult x %d', 1, 'yatra'), 1); | |
| 2632 | 2736 | } |
| 2633 | 2737 | display.textContent = text; |
| 2634 | 2738 | }; |
| 2635 | 2739 | |
| @@ -2715,9 +2819,9 @@ | ||
| 2715 | 2819 | const email = formData.get('email'); |
| 2716 | 2820 | const message = formData.get('message'); |
| 2717 | 2821 | |
| 2718 | 2822 | if (!name || !email || !message) { |
| 2719 | - this.showMessage('error', 'Please fill in all required fields.'); | |
| 2823 | + this.showMessage('error', __('Please fill in all required fields.', 'yatra')); | |
| 2720 | 2824 | return; |
| 2721 | 2825 | } |
| 2722 | 2826 | |
| 2723 | 2827 | // Basic email validation |
| @@ -2722,22 +2826,22 @@ | ||
| 2722 | 2826 | |
| 2723 | 2827 | // Basic email validation |
| 2724 | 2828 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
| 2725 | 2829 | if (!emailRegex.test(email)) { |
| 2726 | - this.showMessage('error', 'Please enter a valid email address.'); | |
| 2830 | + this.showMessage('error', __('Please enter a valid email address.', 'yatra')); | |
| 2727 | 2831 | return; |
| 2728 | 2832 | } |
| 2729 | 2833 | |
| 2730 | 2834 | if (message.length < 10) { |
| 2731 | - this.showMessage('error', 'Please enter a message (at least 10 characters).'); | |
| 2835 | + this.showMessage('error', __('Please enter a message (at least 10 characters).', 'yatra')); | |
| 2732 | 2836 | return; |
| 2733 | 2837 | } |
| 2734 | 2838 | |
| 2735 | 2839 | // Show loading |
| 2736 | 2840 | const submitBtn = this.form.querySelector('.yatra-enquiry-submit'); |
| 2737 | - const originalText = submitBtn ? submitBtn.textContent : 'Send Enquiry'; | |
| 2841 | + const originalText = submitBtn ? submitBtn.textContent : __('Send Enquiry', 'yatra'); | |
| 2738 | 2842 | if (submitBtn) { |
| 2739 | - submitBtn.textContent = 'Sending...'; | |
| 2843 | + submitBtn.textContent = __('Sending...', 'yatra'); | |
| 2740 | 2844 | submitBtn.disabled = true; |
| 2741 | 2845 | } |
| 2742 | 2846 | |
| 2743 | 2847 | // Hide any existing messages |
| @@ -2760,26 +2864,38 @@ | ||
| 2760 | 2864 | const enquiriesUrl = baseUrlRaw.includes('/yatra/v1') |
| 2761 | 2865 | ? `${baseUrlRaw}/enquiries` |
| 2762 | 2866 | : `${baseUrlRaw}/yatra/v1/enquiries`; |
| 2763 | 2867 | |
| 2764 | - fetch(enquiriesUrl, { | |
| 2765 | - method: 'POST', | |
| 2766 | - headers: { | |
| 2767 | - 'Content-Type': 'application/json', | |
| 2768 | - }, | |
| 2769 | - credentials: 'omit', // public endpoint | |
| 2770 | - body: JSON.stringify(payload), | |
| 2771 | - }) | |
| 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 | + }) | |
| 2772 | 2888 | .then(response => response.json()) |
| 2773 | 2889 | .then(result => { |
| 2774 | 2890 | if (result.success) { |
| 2775 | - 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')); | |
| 2776 | 2892 | this.form.reset(); |
| 2777 | 2893 | |
| 2778 | 2894 | // Reset the travelers display |
| 2779 | 2895 | const display = document.getElementById('enquiry-participants-display'); |
| 2780 | 2896 | if (display) { |
| 2781 | - display.textContent = 'Adult x 1'; | |
| 2897 | + display.textContent = __('Adult x 1', 'yatra'); | |
| 2782 | 2898 | } |
| 2783 | 2899 | const adultsInput = document.getElementById('enquiry-adults'); |
| 2784 | 2900 | const childrenInput = document.getElementById('enquiry-children'); |
| 2785 | 2901 | if (adultsInput) adultsInput.value = 1; |
| @@ -2792,14 +2908,14 @@ | ||
| 2792 | 2908 | messageBox.style.display = 'none'; |
| 2793 | 2909 | } |
| 2794 | 2910 | }, 3000); |
| 2795 | 2911 | } else { |
| 2796 | - 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')); | |
| 2797 | 2913 | } |
| 2798 | 2914 | }) |
| 2799 | 2915 | .catch(error => { |
| 2800 | 2916 | console.error('Enquiry submission error:', error); |
| 2801 | - this.showMessage('error', 'An error occurred. Please try again later.'); | |
| 2917 | + this.showMessage('error', __('An error occurred. Please try again later.', 'yatra')); | |
| 2802 | 2918 | }) |
| 2803 | 2919 | .finally(() => { |
| 2804 | 2920 | if (submitBtn) { |
| 2805 | 2921 | submitBtn.textContent = originalText; |
| @@ -2864,21 +2980,33 @@ | ||
| 2864 | 2980 | this.updateSidebarPosition(); // Initial check |
| 2865 | 2981 | } |
| 2866 | 2982 | |
| 2867 | 2983 | getActualSections() { |
| 2868 | - // Base sections that are always present in order | |
| 2869 | - const baseSections = ['overview', 'itinerary', 'included', 'location', 'important-info']; | |
| 2870 | - | |
| 2871 | - // Conditional sections that may or may not be present | |
| 2872 | - const conditionalSections = ['downloads', 'faq', 'trip-story', 'what-makes-special', 'testimonials']; | |
| 2873 | - | |
| 2874 | - // Combine all sections and filter to only include those that actually exist on the page | |
| 2875 | - const allSections = [...baseSections, ...conditionalSections]; | |
| 2876 | - | |
| 2877 | - return allSections.filter(sectionId => { | |
| 2878 | - const element = document.getElementById(sectionId); | |
| 2879 | - 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 | + } | |
| 2880 | 3007 | }); |
| 3008 | + return ids; | |
| 2881 | 3009 | } |
| 2882 | 3010 | |
| 2883 | 3011 | attachEventListeners() { |
| 2884 | 3012 | // Handle clicks on nav items |
| @@ -3050,8 +3178,15 @@ | ||
| 3050 | 3178 | this.lastScrollTop = scrollTop; |
| 3051 | 3179 | } |
| 3052 | 3180 | |
| 3053 | 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. | |
| 3054 | 3189 | const scrollPos = window.scrollY + 150; |
| 3055 | 3190 | let activeSectionId = null; |
| 3056 | 3191 | |
| 3057 | 3192 | // Find which section is currently in view |
| @@ -3057,10 +3192,11 @@ | ||
| 3057 | 3192 | // Find which section is currently in view |
| 3058 | 3193 | this.sections.forEach((sectionId) => { |
| 3059 | 3194 | const section = document.getElementById(sectionId); |
| 3060 | 3195 | if (section) { |
| 3061 | - const sectionTop = section.offsetTop; | |
| 3062 | - const sectionBottom = sectionTop + section.offsetHeight; | |
| 3196 | + const rect = section.getBoundingClientRect(); | |
| 3197 | + const sectionTop = rect.top + window.pageYOffset; | |
| 3198 | + const sectionBottom = sectionTop + rect.height; | |
| 3063 | 3199 | |
| 3064 | 3200 | if (scrollPos >= sectionTop && scrollPos < sectionBottom) { |
| 3065 | 3201 | activeSectionId = sectionId; |
| 3066 | 3202 | } |
| @@ -3207,14 +3343,14 @@ | ||
| 3207 | 3343 | } |
| 3208 | 3344 | } |
| 3209 | 3345 | |
| 3210 | 3346 | if (travelers < 1) { |
| 3211 | - this.showBookingError(btn, 'Please select at least 1 traveler to continue.'); | |
| 3347 | + this.showBookingError(btn, __('Please select at least 1 traveler to continue.', 'yatra')); | |
| 3212 | 3348 | return; |
| 3213 | 3349 | } |
| 3214 | 3350 | |
| 3215 | 3351 | const originalText = btn.innerHTML; |
| 3216 | - 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'); | |
| 3217 | 3353 | btn.disabled = true; |
| 3218 | 3354 | |
| 3219 | 3355 | if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { |
| 3220 | 3356 | console.error('Invalid trip ID:', tripId); |
| @@ -3219,9 +3355,9 @@ | ||
| 3219 | 3355 | if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { |
| 3220 | 3356 | console.error('Invalid trip ID:', tripId); |
| 3221 | 3357 | btn.innerHTML = originalText; |
| 3222 | 3358 | btn.disabled = false; |
| 3223 | - this.showBookingError(btn, 'Unable to process booking. Invalid trip data.'); | |
| 3359 | + this.showBookingError(btn, __('Unable to process booking. Invalid trip data.', 'yatra')); | |
| 3224 | 3360 | return; |
| 3225 | 3361 | } |
| 3226 | 3362 | |
| 3227 | 3363 | const pricingType = btn.getAttribute('data-pricing-type') || 'regular'; |
| @@ -3442,9 +3578,9 @@ | ||
| 3442 | 3578 | currency: window.yatraBookingData?.currency || 'USD', |
| 3443 | 3579 | num_items: sessionPayload.travelers || 1 |
| 3444 | 3580 | }); |
| 3445 | 3581 | |
| 3446 | - 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); | |
| 3447 | 3583 | } |
| 3448 | 3584 | |
| 3449 | 3585 | // Track Book Now click as begin_checkout (Google Analytics 4) |
| 3450 | 3586 | if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) { |
| @@ -3462,9 +3598,9 @@ | ||
| 3462 | 3598 | quantity: sessionPayload.travelers || 1 |
| 3463 | 3599 | }] |
| 3464 | 3600 | }); |
| 3465 | 3601 | |
| 3466 | - 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); | |
| 3467 | 3603 | } |
| 3468 | 3604 | |
| 3469 | 3605 | // Set booking session via REST API |
| 3470 | 3606 | // credentials: 'same-origin' is required to send cookies for session |
| @@ -3495,9 +3631,9 @@ | ||
| 3495 | 3631 | // Error - show message and reset button |
| 3496 | 3632 | console.error('Booking session error:', data); |
| 3497 | 3633 | btn.innerHTML = originalText; |
| 3498 | 3634 | btn.disabled = false; |
| 3499 | - 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')); | |
| 3500 | 3636 | } |
| 3501 | 3637 | }) |
| 3502 | 3638 | .catch(error => { |
| 3503 | 3639 | console.error('Error setting booking session:', error); |
| @@ -3502,9 +3638,9 @@ | ||
| 3502 | 3638 | .catch(error => { |
| 3503 | 3639 | console.error('Error setting booking session:', error); |
| 3504 | 3640 | btn.innerHTML = originalText; |
| 3505 | 3641 | btn.disabled = false; |
| 3506 | - this.showBookingError(btn, 'Unable to process booking. Please try again.'); | |
| 3642 | + this.showBookingError(btn, __('Unable to process booking. Please try again.', 'yatra')); | |
| 3507 | 3643 | }); |
| 3508 | 3644 | } |
| 3509 | 3645 | |
| 3510 | 3646 | initTravelerSelectors() { |
| @@ -3536,18 +3672,21 @@ | ||
| 3536 | 3672 | categoryInputs.forEach((input) => { |
| 3537 | 3673 | const value = parseInt(input.value) || 0; |
| 3538 | 3674 | if (value > 0) { |
| 3539 | 3675 | const row = input.closest('.yatra-quantity-row'); |
| 3540 | - const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3676 | + const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3541 | 3677 | parts.push(`${categoryLabel} x ${value}`); |
| 3542 | 3678 | } |
| 3543 | 3679 | }); |
| 3544 | 3680 | |
| 3545 | 3681 | if (parts.length === 0) { |
| 3546 | - // 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. | |
| 3547 | 3685 | const firstRow = categoryInputs[0]?.closest('.yatra-quantity-row'); |
| 3548 | - const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3549 | - 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); | |
| 3550 | 3689 | } else { |
| 3551 | 3690 | display.textContent = parts.join(', '); |
| 3552 | 3691 | } |
| 3553 | 3692 | }; |
| @@ -3569,10 +3708,10 @@ | ||
| 3569 | 3708 | const isMinus = btn.classList.contains('yatra-quantity-minus'); |
| 3570 | 3709 | const isPlus = btn.classList.contains('yatra-quantity-plus'); |
| 3571 | 3710 | |
| 3572 | 3711 | let current = parseInt(input.value) || 0; |
| 3573 | - const min = parseInt(input.getAttribute('min')) || 0; | |
| 3574 | - 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); | |
| 3575 | 3714 | let newValue = current; |
| 3576 | 3715 | |
| 3577 | 3716 | if (isPlus && current < max) { |
| 3578 | 3717 | newValue = current + 1; |
| @@ -3644,10 +3783,10 @@ | ||
| 3644 | 3783 | const isMinus = btn.classList.contains('yatra-quantity-minus'); |
| 3645 | 3784 | const isPlus = btn.classList.contains('yatra-quantity-plus'); |
| 3646 | 3785 | |
| 3647 | 3786 | let current = parseInt(input.value) || 1; |
| 3648 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 3649 | - 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); | |
| 3650 | 3789 | let newValue = current; |
| 3651 | 3790 | |
| 3652 | 3791 | if (isPlus && current < max) { |
| 3653 | 3792 | newValue = current + 1; |
| @@ -3719,9 +3858,9 @@ | ||
| 3719 | 3858 | categoryInputs.forEach((input) => { |
| 3720 | 3859 | const value = parseInt(input.value) || 0; |
| 3721 | 3860 | if (value > 0) { |
| 3722 | 3861 | const row = input.closest('.yatra-quantity-row'); |
| 3723 | - const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3862 | + const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3724 | 3863 | parts.push(`${categoryLabel} x ${value}`); |
| 3725 | 3864 | |
| 3726 | 3865 | // Also update sidebar inputs if they exist |
| 3727 | 3866 | const categoryId = input.getAttribute('data-category'); |
| @@ -3757,8 +3896,9 @@ | ||
| 3757 | 3896 | // Check for traveler-based pricing first |
| 3758 | 3897 | const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`); |
| 3759 | 3898 | let totalPrice = 0; |
| 3760 | 3899 | let totalTravelers = 0; |
| 3900 | + let totalGroups = 0; // per-group categories: number of group prices charged | |
| 3761 | 3901 | |
| 3762 | 3902 | if (categoryInputs.length > 0) { |
| 3763 | 3903 | // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode) |
| 3764 | 3904 | categoryInputs.forEach((input) => { |
| @@ -3765,12 +3905,16 @@ | ||
| 3765 | 3905 | const quantity = parseInt(input.value) || 0; |
| 3766 | 3906 | const row = input.closest('.yatra-quantity-row'); |
| 3767 | 3907 | const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0; |
| 3768 | 3908 | const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person'; |
| 3769 | - | |
| 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 | + | |
| 3770 | 3912 | if (pricingMode === 'per_group') { |
| 3771 | 3913 | if (quantity > 0) { |
| 3772 | - totalPrice += price; | |
| 3914 | + const blocks = (groupOverflow === 'per_block' && maxPax > 0) ? Math.ceil(quantity / maxPax) : 1; | |
| 3915 | + totalPrice += price * blocks; | |
| 3916 | + totalGroups += blocks; | |
| 3773 | 3917 | } |
| 3774 | 3918 | } else { |
| 3775 | 3919 | totalPrice += quantity * price; |
| 3776 | 3920 | } |
| @@ -3798,14 +3942,21 @@ | ||
| 3798 | 3942 | } else { |
| 3799 | 3943 | totalAmountElement.textContent = String(finalPrice.toFixed(2)); |
| 3800 | 3944 | } |
| 3801 | 3945 | |
| 3802 | - // 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). | |
| 3803 | 3950 | const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]'); |
| 3804 | - 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; | |
| 3805 | 3953 | if (headerSaleEl) { |
| 3954 | + const headerUnit = headerPriceWrap.getAttribute('data-price-unit') || 'per_person'; | |
| 3806 | 3955 | let perPerson = 0; |
| 3807 | - if (totalTravelers > 0) { | |
| 3956 | + if (headerUnit === 'per_group') { | |
| 3957 | + perPerson = totalGroups > 0 ? finalPrice / totalGroups : (totalPrice > 0 ? totalPrice : 0); | |
| 3958 | + } else if (totalTravelers > 0) { | |
| 3808 | 3959 | perPerson = finalPrice / totalTravelers; |
| 3809 | 3960 | } else if (totalPrice > 0) { |
| 3810 | 3961 | perPerson = totalPrice; |
| 3811 | 3962 | } |
| @@ -3819,11 +3970,13 @@ | ||
| 3819 | 3970 | const noteElement = this.section.querySelector( |
| 3820 | 3971 | `.yatra-card-total-note[data-item="${itemIndex}"]` |
| 3821 | 3972 | ); |
| 3822 | 3973 | if (noteElement) { |
| 3823 | - let travelerText = totalTravelers === 1 | |
| 3824 | - ? 'for 1 traveler' | |
| 3825 | - : `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 | + ); | |
| 3826 | 3979 | |
| 3827 | 3980 | // Add group discount info if applied |
| 3828 | 3981 | if (groupDiscountApplied && groupDiscountAmount > 0) { |
| 3829 | 3982 | const discountText = (typeof yatra_format_price_js === 'function') |
| @@ -3828,9 +3981,13 @@ | ||
| 3828 | 3981 | if (groupDiscountApplied && groupDiscountAmount > 0) { |
| 3829 | 3982 | const discountText = (typeof yatra_format_price_js === 'function') |
| 3830 | 3983 | ? yatra_format_price_js(groupDiscountAmount) |
| 3831 | 3984 | : String(groupDiscountAmount.toFixed(2)); |
| 3832 | - travelerText += ` (Group discount: -${discountText})`; | |
| 3985 | + travelerText += sprintf( | |
| 3986 | + /* translators: %s: formatted discount amount. */ | |
| 3987 | + __(' (Group discount: -%s)', 'yatra'), | |
| 3988 | + discountText | |
| 3989 | + ); | |
| 3833 | 3990 | } |
| 3834 | 3991 | noteElement.textContent = travelerText; |
| 3835 | 3992 | } |
| 3836 | 3993 | |
| @@ -3945,10 +4102,14 @@ | ||
| 3945 | 4102 | } |
| 3946 | 4103 | |
| 3947 | 4104 | // Update badge content |
| 3948 | 4105 | const discountText = discountResult.discountType === 'percentage' |
| 3949 | - ? `${discountResult.discountPercentage}% group discount applied!` | |
| 3950 | - : `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'); | |
| 3951 | 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}`; |
| 3952 | 4113 | } else if (badge) { |
| 3953 | 4114 | badge.remove(); |
| 3954 | 4115 | } |
| @@ -4028,9 +4189,9 @@ | ||
| 4028 | 4189 | } |
| 4029 | 4190 | |
| 4030 | 4191 | // Show success feedback |
| 4031 | 4192 | const originalText = button.textContent; |
| 4032 | - button.textContent = 'Selected!'; | |
| 4193 | + button.textContent = __('Selected!', 'yatra'); | |
| 4033 | 4194 | button.style.background = '#10b981'; |
| 4034 | 4195 | setTimeout(() => { |
| 4035 | 4196 | button.textContent = originalText; |
| 4036 | 4197 | button.style.background = ''; |
| @@ -4055,9 +4216,11 @@ | ||
| 4055 | 4216 | const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined; |
| 4056 | 4217 | |
| 4057 | 4218 | const originalHTML = btn.innerHTML; |
| 4058 | 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> '; |
| 4059 | - 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'); | |
| 4060 | 4223 | btn.disabled = true; |
| 4061 | 4224 | |
| 4062 | 4225 | const url = buildAvailabilityTemplateUrl(tripId, { |
| 4063 | 4226 | date: travelDate, |
| @@ -4076,9 +4239,9 @@ | ||
| 4076 | 4239 | credentials: 'omit', |
| 4077 | 4240 | }) |
| 4078 | 4241 | .then((resp) => { |
| 4079 | 4242 | if (!resp.ok) { |
| 4080 | - throw new Error('Load more failed'); | |
| 4243 | + throw new Error(__('Load more failed', 'yatra')); | |
| 4081 | 4244 | } |
| 4082 | 4245 | return resp.json(); |
| 4083 | 4246 | }) |
| 4084 | 4247 | .then((data) => { |
| @@ -4115,9 +4278,11 @@ | ||
| 4115 | 4278 | return; |
| 4116 | 4279 | } |
| 4117 | 4280 | |
| 4118 | 4281 | const remaining = Math.max(0, total - displayed); |
| 4119 | - 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}`; | |
| 4120 | 4285 | btn.disabled = false; |
| 4121 | 4286 | |
| 4122 | 4287 | loadMoreContainer.dataset.currentPage = String(nextPage); |
| 4123 | 4288 | loadMoreContainer.dataset.loaded = String(displayed); |
| @@ -4123,9 +4288,17 @@ | ||
| 4123 | 4288 | loadMoreContainer.dataset.loaded = String(displayed); |
| 4124 | 4289 | loadMoreContainer.dataset.total = String(total); |
| 4125 | 4290 | |
| 4126 | 4291 | if (countInfo) { |
| 4127 | - 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 | + ); | |
| 4128 | 4301 | } |
| 4129 | 4302 | }) |
| 4130 | 4303 | .catch((err) => { |
| 4131 | 4304 | console.error('[Yatra] Load more failed:', err); |
| @@ -4289,9 +4462,9 @@ | ||
| 4289 | 4462 | if (!this.toggleAllBtn) return; |
| 4290 | 4463 | this.toggleAllBtn.classList.toggle('is-all-expanded', isExpanded); |
| 4291 | 4464 | const toggleText = this.toggleAllBtn.querySelector('.toggle-text'); |
| 4292 | 4465 | if (toggleText) { |
| 4293 | - toggleText.textContent = isExpanded ? 'Collapse All' : 'Expand All'; | |
| 4466 | + toggleText.textContent = isExpanded ? __('Collapse All', 'yatra') : __('Expand All', 'yatra'); | |
| 4294 | 4467 | } |
| 4295 | 4468 | } |
| 4296 | 4469 | } |
| 4297 | 4470 | |
| @@ -4404,9 +4577,10 @@ | ||
| 4404 | 4577 | const originalText = submitBtn.innerHTML; |
| 4405 | 4578 | |
| 4406 | 4579 | // Disable button and show loading |
| 4407 | 4580 | submitBtn.disabled = true; |
| 4408 | - submitBtn.innerHTML = '<span class="yatra-loading-spinner"></span> Submitting...'; | |
| 4581 | + submitBtn.innerHTML = | |
| 4582 | + '<span class="yatra-loading-spinner"></span> ' + __('Submitting...', 'yatra'); | |
| 4409 | 4583 | |
| 4410 | 4584 | try { |
| 4411 | 4585 | const formData = new FormData(this.form); |
| 4412 | 4586 | formData.append('action', 'yatra_submit_review'); |
| @@ -4420,9 +4594,9 @@ | ||
| 4420 | 4594 | const result = await response.json(); |
| 4421 | 4595 | |
| 4422 | 4596 | if (result.success) { |
| 4423 | 4597 | // Show success message |
| 4424 | - this.showMessage('success', result.data?.message || 'Thank you for your review!'); | |
| 4598 | + this.showMessage('success', result.data?.message || __('Thank you for your review!', 'yatra')); | |
| 4425 | 4599 | |
| 4426 | 4600 | // Check if this was an edit or new submission |
| 4427 | 4601 | const isEdit = this.form.querySelector('input[name="action_type"]')?.value === 'edit'; |
| 4428 | 4602 | |
| @@ -4439,13 +4613,13 @@ | ||
| 4439 | 4613 | ratingLabels.forEach(label => label.classList.add('selected')); |
| 4440 | 4614 | } |
| 4441 | 4615 | } |
| 4442 | 4616 | } else { |
| 4443 | - 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')); | |
| 4444 | 4618 | } |
| 4445 | 4619 | } catch (error) { |
| 4446 | 4620 | console.error('Review submission error:', error); |
| 4447 | - this.showMessage('error', 'An error occurred. Please try again.'); | |
| 4621 | + this.showMessage('error', __('An error occurred. Please try again.', 'yatra')); | |
| 4448 | 4622 | } finally { |
| 4449 | 4623 | submitBtn.disabled = false; |
| 4450 | 4624 | submitBtn.innerHTML = originalText; |
| 4451 | 4625 | } |
| @@ -4619,16 +4793,13 @@ | ||
| 4619 | 4793 | * Handles save/remove trip from wishlist functionality |
| 4620 | 4794 | */ |
| 4621 | 4795 | class WishlistHandler { |
| 4622 | 4796 | constructor() { |
| 4623 | - if (!window.yatraTripData?.wishlistEnabled) { | |
| 4624 | - return; | |
| 4625 | - } | |
| 4626 | - this.apiUrl = window.yatraTripData?.apiUrl || '/wp-json/yatra/v1'; | |
| 4627 | - this.nonce = window.yatraTripData?.nonce || ''; | |
| 4628 | - this.tripId = window.yatraTripData?.tripId || 0; | |
| 4629 | - this.isLoggedIn = window.yatraTripData?.isLoggedIn || false; | |
| 4630 | - 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; | |
| 4631 | 4802 | } |
| 4632 | 4803 | |
| 4633 | 4804 | init() { |
| 4634 | 4805 | // Check if user is logged in and update button states |
| @@ -4711,15 +4882,15 @@ | ||
| 4711 | 4882 | |
| 4712 | 4883 | const data = await response.json(); |
| 4713 | 4884 | if (data.success) { |
| 4714 | 4885 | this.updateButtonState(true, tid); |
| 4715 | - this.showMessage(data.message || 'Trip saved to wishlist', 'success', btn); | |
| 4886 | + this.showMessage(data.message || __('Trip saved to wishlist', 'yatra'), 'success', btn); | |
| 4716 | 4887 | } else { |
| 4717 | - this.showMessage(data.message || 'Failed to save trip', 'error', btn); | |
| 4888 | + this.showMessage(data.message || __('Failed to save trip', 'yatra'), 'error', btn); | |
| 4718 | 4889 | } |
| 4719 | 4890 | } catch (error) { |
| 4720 | 4891 | console.error('Error saving trip:', error); |
| 4721 | - this.showMessage('An error occurred. Please try again.', 'error', btn); | |
| 4892 | + this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn); | |
| 4722 | 4893 | } finally { |
| 4723 | 4894 | btn.disabled = false; |
| 4724 | 4895 | btn.classList.remove('loading'); |
| 4725 | 4896 | } |
| @@ -4741,15 +4912,15 @@ | ||
| 4741 | 4912 | |
| 4742 | 4913 | const data = await response.json(); |
| 4743 | 4914 | if (data.success) { |
| 4744 | 4915 | this.updateButtonState(false, tid); |
| 4745 | - this.showMessage(data.message || 'Trip removed from wishlist', 'success', btn); | |
| 4916 | + this.showMessage(data.message || __('Trip removed from wishlist', 'yatra'), 'success', btn); | |
| 4746 | 4917 | } else { |
| 4747 | - this.showMessage(data.message || 'Failed to remove trip', 'error', btn); | |
| 4918 | + this.showMessage(data.message || __('Failed to remove trip', 'yatra'), 'error', btn); | |
| 4748 | 4919 | } |
| 4749 | 4920 | } catch (error) { |
| 4750 | 4921 | console.error('Error removing trip:', error); |
| 4751 | - this.showMessage('An error occurred. Please try again.', 'error', btn); | |
| 4922 | + this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn); | |
| 4752 | 4923 | } finally { |
| 4753 | 4924 | btn.disabled = false; |
| 4754 | 4925 | btn.classList.remove('loading'); |
| 4755 | 4926 | } |
| @@ -4763,9 +4934,9 @@ | ||
| 4763 | 4934 | const buttons = document.querySelectorAll(selector); |
| 4764 | 4935 | buttons.forEach(btn => { |
| 4765 | 4936 | if (isSaved) { |
| 4766 | 4937 | btn.classList.add('saved', 'is-saved'); |
| 4767 | - btn.setAttribute('aria-label', 'Remove from favorites'); | |
| 4938 | + btn.setAttribute('aria-label', __('Remove from favorites', 'yatra')); | |
| 4768 | 4939 | // Fill the heart icon and change color |
| 4769 | 4940 | const svg = btn.querySelector('svg'); |
| 4770 | 4941 | if (svg) { |
| 4771 | 4942 | svg.style.fill = 'currentColor'; |
| @@ -4773,9 +4944,9 @@ | ||
| 4773 | 4944 | btn.style.color = '#ef4444'; |
| 4774 | 4945 | } |
| 4775 | 4946 | } else { |
| 4776 | 4947 | btn.classList.remove('saved', 'is-saved'); |
| 4777 | - btn.setAttribute('aria-label', 'Add to favorites'); | |
| 4948 | + btn.setAttribute('aria-label', __('Add to favorites', 'yatra')); | |
| 4778 | 4949 | // Unfill the heart icon |
| 4779 | 4950 | const svg = btn.querySelector('svg'); |
| 4780 | 4951 | if (svg) { |
| 4781 | 4952 | svg.style.fill = 'none'; |
| @@ -4799,13 +4970,17 @@ | ||
| 4799 | 4970 | if (modalContent) { |
| 4800 | 4971 | const loginMessage = document.createElement('div'); |
| 4801 | 4972 | loginMessage.className = 'yatra-login-prompt'; |
| 4802 | 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'); | |
| 4803 | 4978 | loginMessage.innerHTML = ` |
| 4804 | - <h3 style="margin-bottom: 20px;">Login Required</h3> | |
| 4805 | - <p style="margin-bottom: 30px;">Please login to save trips to your wishlist.</p> | |
| 4806 | - <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">Login</a> | |
| 4807 | - <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> | |
| 4808 | 4983 | `; |
| 4809 | 4984 | modalContent.innerHTML = ''; |
| 4810 | 4985 | modalContent.appendChild(loginMessage); |
| 4811 | 4986 | } |
| @@ -4810,9 +4985,9 @@ | ||
| 4810 | 4985 | modalContent.appendChild(loginMessage); |
| 4811 | 4986 | } |
| 4812 | 4987 | } else { |
| 4813 | 4988 | // Fallback: show alert and redirect to login |
| 4814 | - 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'))) { | |
| 4815 | 4990 | window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php'; |
| 4816 | 4991 | } |
| 4817 | 4992 | } |
| 4818 | 4993 | } |
| @@ -5062,15 +5237,15 @@ | ||
| 5062 | 5237 | const videoUrl = link.getAttribute('href'); |
| 5063 | 5238 | if (!videoUrl || videoUrl === '#') return; |
| 5064 | 5239 | |
| 5065 | 5240 | const img = overlay.closest('.yatra-media-card').querySelector('img'); |
| 5066 | - const videoTitle = img ? img.alt : 'Itinerary Video'; | |
| 5241 | + const videoTitle = img ? img.alt : __('Itinerary Video', 'yatra'); | |
| 5067 | 5242 | |
| 5068 | 5243 | // Extract video ID from URL |
| 5069 | 5244 | let videoId = ''; |
| 5070 | 5245 | if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { |
| 5071 | 5246 | // YouTube video ID extraction |
| 5072 | - 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?#\/]+)/); | |
| 5073 | 5248 | videoId = matches ? matches[1] : ''; |
| 5074 | 5249 | } else if (videoUrl.includes('vimeo.com')) { |
| 5075 | 5250 | // Vimeo video ID extraction |
| 5076 | 5251 | const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/); |
| @@ -5183,9 +5358,9 @@ | ||
| 5183 | 5358 | try { |
| 5184 | 5359 | // Get download URL with signature using centralized helper |
| 5185 | 5360 | const response = await window.YatraApiHelper.getDownloadInfo(downloadId); |
| 5186 | 5361 | if (!response.ok) { |
| 5187 | - throw new Error('Download failed'); | |
| 5362 | + throw new Error(__('Download failed', 'yatra')); | |
| 5188 | 5363 | } |
| 5189 | 5364 | |
| 5190 | 5365 | const data = await response.json(); |
| 5191 | 5366 | |
| @@ -5202,9 +5377,9 @@ | ||
| 5202 | 5377 | } |
| 5203 | 5378 | } catch (error) { |
| 5204 | 5379 | console.error('Download error:', error); |
| 5205 | 5380 | // Show error message to user |
| 5206 | - this.showNotification('Download failed. Please try again.', 'error'); | |
| 5381 | + this.showNotification(__('Download failed. Please try again.', 'yatra'), 'error'); | |
| 5207 | 5382 | } |
| 5208 | 5383 | } |
| 5209 | 5384 | |
| 5210 | 5385 | async previewFile(downloadId) { |
| @@ -5214,15 +5389,15 @@ | ||
| 5214 | 5389 | |
| 5215 | 5390 | // Get download info using centralized helper |
| 5216 | 5391 | const response = await window.YatraApiHelper.getDownloadInfo(downloadId); |
| 5217 | 5392 | if (!response.ok) { |
| 5218 | - throw new Error('Failed to get file info'); | |
| 5393 | + throw new Error(__('Failed to get file info', 'yatra')); | |
| 5219 | 5394 | } |
| 5220 | 5395 | |
| 5221 | 5396 | const data = await response.json(); |
| 5222 | 5397 | |
| 5223 | 5398 | if (data.download_url) { |
| 5224 | - this.loadPreview(data.download_url, data.title || 'Preview'); | |
| 5399 | + this.loadPreview(data.download_url, data.title || __('Preview', 'yatra')); | |
| 5225 | 5400 | } else { |
| 5226 | 5401 | throw new Error('File URL not found'); |
| 5227 | 5402 | } |
| 5228 | 5403 | } catch (error) { |
| @@ -5227,9 +5402,9 @@ | ||
| 5227 | 5402 | } |
| 5228 | 5403 | } catch (error) { |
| 5229 | 5404 | console.error('Preview error:', error); |
| 5230 | 5405 | this.closePreviewModal(); |
| 5231 | - this.showNotification('Failed to load preview. Please try again.', 'error'); | |
| 5406 | + this.showNotification(__('Failed to load preview. Please try again.', 'yatra'), 'error'); | |
| 5232 | 5407 | } |
| 5233 | 5408 | } |
| 5234 | 5409 | |
| 5235 | 5410 | loadPreview(fileUrl, title) { |
| @@ -5257,13 +5432,15 @@ | ||
| 5257 | 5432 | previewContent.innerHTML = ` |
| 5258 | 5433 | <iframe src="${fileUrl}" style="width: 100%; height: 500px; border: none; border-radius: 8px;"></iframe> |
| 5259 | 5434 | `; |
| 5260 | 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'); | |
| 5261 | 5438 | previewContent.innerHTML = ` |
| 5262 | 5439 | <div style="text-align: center; padding: 40px;"> |
| 5263 | 5440 | <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">📄</div> |
| 5264 | - <p style="color: #6b7280; margin-bottom: 16px;">Preview not available for this file type</p> | |
| 5265 | - <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> | |
| 5266 | 5443 | </div> |
| 5267 | 5444 | `; |
| 5268 | 5445 | } |
| 5269 | 5446 | } |
| @@ -5285,12 +5462,13 @@ | ||
| 5285 | 5462 | |
| 5286 | 5463 | // Reset loading state |
| 5287 | 5464 | const previewContent = document.getElementById('preview-content'); |
| 5288 | 5465 | if (previewContent) { |
| 5466 | + const loadingPreviewLabel = __('Loading preview...', 'yatra'); | |
| 5289 | 5467 | previewContent.innerHTML = ` |
| 5290 | 5468 | <div class="yatra-preview-loading"> |
| 5291 | 5469 | <div class="yatra-preview-spinner"></div> |
| 5292 | - <p>Loading preview...</p> | |
| 5470 | + <p>${loadingPreviewLabel}</p> | |
| 5293 | 5471 | </div> |
| 5294 | 5472 | `; |
| 5295 | 5473 | } |
| 5296 | 5474 | } |