PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | app/Helpers/FormatHelper.php +564 -80 3.0.2.6 → 3.0.16 View file →
@@ -3,8 +3,10 @@
3 3 declare(strict_types=1);
4 4
5 5 namespace Yatra\Helpers;
6 6
7 +use Yatra\Services\SettingsService;
8 +
7 9 /**
8 10 * Format Helper
9 11 *
10 12 * Utility functions for formatting data.
@@ -20,61 +22,108 @@
20 22
21 23 /**
22 24 * Country codes to names mapping
23 25 */
24 - private const COUNTRY_NAMES = [
25 - 'AF' => 'Afghanistan', 'AL' => 'Albania', 'DZ' => 'Algeria', 'AD' => 'Andorra',
26 - 'AO' => 'Angola', 'AG' => 'Antigua and Barbuda', 'AR' => 'Argentina', 'AM' => 'Armenia',
27 - 'AU' => 'Australia', 'AT' => 'Austria', 'AZ' => 'Azerbaijan', 'BS' => 'Bahamas',
28 - 'BH' => 'Bahrain', 'BD' => 'Bangladesh', 'BB' => 'Barbados', 'BY' => 'Belarus',
29 - 'BE' => 'Belgium', 'BZ' => 'Belize', 'BJ' => 'Benin', 'BT' => 'Bhutan',
30 - 'BO' => 'Bolivia', 'BA' => 'Bosnia and Herzegovina', 'BW' => 'Botswana', 'BR' => 'Brazil',
31 - 'BN' => 'Brunei', 'BG' => 'Bulgaria', 'BF' => 'Burkina Faso', 'BI' => 'Burundi',
32 - 'KH' => 'Cambodia', 'CM' => 'Cameroon', 'CA' => 'Canada', 'CV' => 'Cape Verde',
33 - 'CF' => 'Central African Republic', 'TD' => 'Chad', 'CL' => 'Chile', 'CN' => 'China',
34 - 'CO' => 'Colombia', 'KM' => 'Comoros', 'CG' => 'Congo', 'CD' => 'DR Congo',
35 - 'CR' => 'Costa Rica', 'CI' => 'Ivory Coast', 'HR' => 'Croatia', 'CU' => 'Cuba',
36 - 'CY' => 'Cyprus', 'CZ' => 'Czech Republic', 'DK' => 'Denmark', 'DJ' => 'Djibouti',
37 - 'DM' => 'Dominica', 'DO' => 'Dominican Republic', 'EC' => 'Ecuador', 'EG' => 'Egypt',
38 - 'SV' => 'El Salvador', 'GQ' => 'Equatorial Guinea', 'ER' => 'Eritrea', 'EE' => 'Estonia',
39 - 'SZ' => 'Eswatini', 'ET' => 'Ethiopia', 'FJ' => 'Fiji', 'FI' => 'Finland',
40 - 'FR' => 'France', 'GA' => 'Gabon', 'GM' => 'Gambia', 'GE' => 'Georgia',
41 - 'DE' => 'Germany', 'GH' => 'Ghana', 'GR' => 'Greece', 'GD' => 'Grenada',
42 - 'GT' => 'Guatemala', 'GN' => 'Guinea', 'GW' => 'Guinea-Bissau', 'GY' => 'Guyana',
43 - 'HT' => 'Haiti', 'HN' => 'Honduras', 'HU' => 'Hungary', 'IS' => 'Iceland',
44 - 'IN' => 'India', 'ID' => 'Indonesia', 'IR' => 'Iran', 'IQ' => 'Iraq',
45 - 'IE' => 'Ireland', 'IL' => 'Israel', 'IT' => 'Italy', 'JM' => 'Jamaica',
46 - 'JP' => 'Japan', 'JO' => 'Jordan', 'KZ' => 'Kazakhstan', 'KE' => 'Kenya',
47 - 'KI' => 'Kiribati', 'KP' => 'North Korea', 'KR' => 'South Korea', 'KW' => 'Kuwait',
48 - 'KG' => 'Kyrgyzstan', 'LA' => 'Laos', 'LV' => 'Latvia', 'LB' => 'Lebanon',
49 - 'LS' => 'Lesotho', 'LR' => 'Liberia', 'LY' => 'Libya', 'LI' => 'Liechtenstein',
50 - 'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'MG' => 'Madagascar', 'MW' => 'Malawi',
51 - 'MY' => 'Malaysia', 'MV' => 'Maldives', 'ML' => 'Mali', 'MT' => 'Malta',
52 - 'MH' => 'Marshall Islands', 'MR' => 'Mauritania', 'MU' => 'Mauritius', 'MX' => 'Mexico',
53 - 'FM' => 'Micronesia', 'MD' => 'Moldova', 'MC' => 'Monaco', 'MN' => 'Mongolia',
54 - 'ME' => 'Montenegro', 'MA' => 'Morocco', 'MZ' => 'Mozambique', 'MM' => 'Myanmar',
55 - 'NA' => 'Namibia', 'NR' => 'Nauru', 'NP' => 'Nepal', 'NL' => 'Netherlands',
56 - 'NZ' => 'New Zealand', 'NI' => 'Nicaragua', 'NE' => 'Niger', 'NG' => 'Nigeria',
57 - 'MK' => 'North Macedonia', 'NO' => 'Norway', 'OM' => 'Oman', 'PK' => 'Pakistan',
58 - 'PW' => 'Palau', 'PS' => 'Palestine', 'PA' => 'Panama', 'PG' => 'Papua New Guinea',
59 - 'PY' => 'Paraguay', 'PE' => 'Peru', 'PH' => 'Philippines', 'PL' => 'Poland',
60 - 'PT' => 'Portugal', 'QA' => 'Qatar', 'RO' => 'Romania', 'RU' => 'Russia',
61 - 'RW' => 'Rwanda', 'KN' => 'Saint Kitts and Nevis', 'LC' => 'Saint Lucia',
62 - 'VC' => 'Saint Vincent and the Grenadines', 'WS' => 'Samoa', 'SM' => 'San Marino',
63 - 'ST' => 'Sao Tome and Principe', 'SA' => 'Saudi Arabia', 'SN' => 'Senegal',
64 - 'RS' => 'Serbia', 'SC' => 'Seychelles', 'SL' => 'Sierra Leone', 'SG' => 'Singapore',
65 - 'SK' => 'Slovakia', 'SI' => 'Slovenia', 'SB' => 'Solomon Islands', 'SO' => 'Somalia',
66 - 'ZA' => 'South Africa', 'SS' => 'South Sudan', 'ES' => 'Spain', 'LK' => 'Sri Lanka',
67 - 'SD' => 'Sudan', 'SR' => 'Suriname', 'SE' => 'Sweden', 'CH' => 'Switzerland',
68 - 'SY' => 'Syria', 'TW' => 'Taiwan', 'TJ' => 'Tajikistan', 'TZ' => 'Tanzania',
69 - 'TH' => 'Thailand', 'TL' => 'Timor-Leste', 'TG' => 'Togo', 'TO' => 'Tonga',
70 - 'TT' => 'Trinidad and Tobago', 'TN' => 'Tunisia', 'TR' => 'Turkey', 'TM' => 'Turkmenistan',
71 - 'TV' => 'Tuvalu', 'UG' => 'Uganda', 'UA' => 'Ukraine', 'AE' => 'United Arab Emirates',
72 - 'GB' => 'United Kingdom', 'US' => 'United States', 'UY' => 'Uruguay', 'UZ' => 'Uzbekistan',
73 - 'VU' => 'Vanuatu', 'VA' => 'Vatican City', 'VE' => 'Venezuela', 'VN' => 'Vietnam',
74 - 'YE' => 'Yemen', 'ZM' => 'Zambia', 'ZW' => 'Zimbabwe',
75 - ];
26 + /**
27 + * Country code => translated display name.
28 + *
29 + * Names are wrapped in __() at the definition so every consumer (booking
30 + * form, admin, confirmation) shows localized names, and so all of them are
31 + * extractable into the translation catalog. Cached per request.
32 + */
33 + private static ?array $countryNamesCache = null;
76 34
35 + private static function countryNames(): array
36 + {
37 + if (self::$countryNamesCache !== null) {
38 + return self::$countryNamesCache;
39 + }
40 +
41 + return self::$countryNamesCache = [
42 +
43 + 'AF' => __('Afghanistan', 'yatra'), 'AL' => __('Albania', 'yatra'), 'DZ' => __('Algeria', 'yatra'), 'AD' => __('Andorra', 'yatra'),
44 + 'AO' => __('Angola', 'yatra'), 'AG' => __('Antigua and Barbuda', 'yatra'), 'AR' => __('Argentina', 'yatra'), 'AM' => __('Armenia', 'yatra'),
45 + 'AU' => __('Australia', 'yatra'), 'AT' => __('Austria', 'yatra'), 'AZ' => __('Azerbaijan', 'yatra'), 'BS' => __('Bahamas', 'yatra'),
46 + 'BH' => __('Bahrain', 'yatra'), 'BD' => __('Bangladesh', 'yatra'), 'BB' => __('Barbados', 'yatra'), 'BY' => __('Belarus', 'yatra'),
47 + 'BE' => __('Belgium', 'yatra'), 'BZ' => __('Belize', 'yatra'), 'BJ' => __('Benin', 'yatra'), 'BT' => __('Bhutan', 'yatra'),
48 + 'BO' => __('Bolivia', 'yatra'), 'BA' => __('Bosnia and Herzegovina', 'yatra'), 'BW' => __('Botswana', 'yatra'), 'BR' => __('Brazil', 'yatra'),
49 + 'BN' => __('Brunei', 'yatra'), 'BG' => __('Bulgaria', 'yatra'), 'BF' => __('Burkina Faso', 'yatra'), 'BI' => __('Burundi', 'yatra'),
50 + 'KH' => __('Cambodia', 'yatra'), 'CM' => __('Cameroon', 'yatra'), 'CA' => __('Canada', 'yatra'), 'CV' => __('Cape Verde', 'yatra'),
51 + 'CF' => __('Central African Republic', 'yatra'), 'TD' => __('Chad', 'yatra'), 'CL' => __('Chile', 'yatra'), 'CN' => __('China', 'yatra'),
52 + 'CO' => __('Colombia', 'yatra'), 'KM' => __('Comoros', 'yatra'), 'CG' => __('Congo', 'yatra'), 'CD' => __('DR Congo', 'yatra'),
53 + 'CR' => __('Costa Rica', 'yatra'), 'CI' => __('Ivory Coast', 'yatra'), 'HR' => __('Croatia', 'yatra'), 'CU' => __('Cuba', 'yatra'),
54 + 'CY' => __('Cyprus', 'yatra'), 'CZ' => __('Czech Republic', 'yatra'), 'DK' => __('Denmark', 'yatra'), 'DJ' => __('Djibouti', 'yatra'),
55 + 'DM' => __('Dominica', 'yatra'), 'DO' => __('Dominican Republic', 'yatra'), 'EC' => __('Ecuador', 'yatra'), 'EG' => __('Egypt', 'yatra'),
56 + 'SV' => __('El Salvador', 'yatra'), 'GQ' => __('Equatorial Guinea', 'yatra'), 'ER' => __('Eritrea', 'yatra'), 'EE' => __('Estonia', 'yatra'),
57 + 'SZ' => __('Eswatini', 'yatra'), 'ET' => __('Ethiopia', 'yatra'), 'FJ' => __('Fiji', 'yatra'), 'FI' => __('Finland', 'yatra'),
58 + 'FR' => __('France', 'yatra'), 'GA' => __('Gabon', 'yatra'), 'GM' => __('Gambia', 'yatra'), 'GE' => __('Georgia', 'yatra'),
59 + 'DE' => __('Germany', 'yatra'), 'GH' => __('Ghana', 'yatra'), 'GR' => __('Greece', 'yatra'), 'GD' => __('Grenada', 'yatra'),
60 + 'GT' => __('Guatemala', 'yatra'), 'GN' => __('Guinea', 'yatra'), 'GW' => __('Guinea-Bissau', 'yatra'), 'GY' => __('Guyana', 'yatra'),
61 + 'HT' => __('Haiti', 'yatra'), 'HN' => __('Honduras', 'yatra'), 'HU' => __('Hungary', 'yatra'), 'IS' => __('Iceland', 'yatra'),
62 + 'IN' => __('India', 'yatra'), 'ID' => __('Indonesia', 'yatra'), 'IR' => __('Iran', 'yatra'), 'IQ' => __('Iraq', 'yatra'),
63 + 'IE' => __('Ireland', 'yatra'), 'IL' => __('Israel', 'yatra'), 'IT' => __('Italy', 'yatra'), 'JM' => __('Jamaica', 'yatra'),
64 + 'JP' => __('Japan', 'yatra'), 'JO' => __('Jordan', 'yatra'), 'KZ' => __('Kazakhstan', 'yatra'), 'KE' => __('Kenya', 'yatra'),
65 + 'KI' => __('Kiribati', 'yatra'), 'KP' => __('North Korea', 'yatra'), 'KR' => __('South Korea', 'yatra'), 'KW' => __('Kuwait', 'yatra'),
66 + 'KG' => __('Kyrgyzstan', 'yatra'), 'LA' => __('Laos', 'yatra'), 'LV' => __('Latvia', 'yatra'), 'LB' => __('Lebanon', 'yatra'),
67 + 'LS' => __('Lesotho', 'yatra'), 'LR' => __('Liberia', 'yatra'), 'LY' => __('Libya', 'yatra'), 'LI' => __('Liechtenstein', 'yatra'),
68 + 'LT' => __('Lithuania', 'yatra'), 'LU' => __('Luxembourg', 'yatra'), 'MG' => __('Madagascar', 'yatra'), 'MW' => __('Malawi', 'yatra'),
69 + 'MY' => __('Malaysia', 'yatra'), 'MV' => __('Maldives', 'yatra'), 'ML' => __('Mali', 'yatra'), 'MT' => __('Malta', 'yatra'),
70 + 'MH' => __('Marshall Islands', 'yatra'), 'MR' => __('Mauritania', 'yatra'), 'MU' => __('Mauritius', 'yatra'), 'MX' => __('Mexico', 'yatra'),
71 + 'FM' => __('Micronesia', 'yatra'), 'MD' => __('Moldova', 'yatra'), 'MC' => __('Monaco', 'yatra'), 'MN' => __('Mongolia', 'yatra'),
72 + 'ME' => __('Montenegro', 'yatra'), 'MA' => __('Morocco', 'yatra'), 'MZ' => __('Mozambique', 'yatra'), 'MM' => __('Myanmar', 'yatra'),
73 + 'NA' => __('Namibia', 'yatra'), 'NR' => __('Nauru', 'yatra'), 'NP' => __('Nepal', 'yatra'), 'NL' => __('Netherlands', 'yatra'),
74 + 'NZ' => __('New Zealand', 'yatra'), 'NI' => __('Nicaragua', 'yatra'), 'NE' => __('Niger', 'yatra'), 'NG' => __('Nigeria', 'yatra'),
75 + 'MK' => __('North Macedonia', 'yatra'), 'NO' => __('Norway', 'yatra'), 'OM' => __('Oman', 'yatra'), 'PK' => __('Pakistan', 'yatra'),
76 + 'PW' => __('Palau', 'yatra'), 'PS' => __('Palestine', 'yatra'), 'PA' => __('Panama', 'yatra'), 'PG' => __('Papua New Guinea', 'yatra'),
77 + 'PY' => __('Paraguay', 'yatra'), 'PE' => __('Peru', 'yatra'), 'PH' => __('Philippines', 'yatra'), 'PL' => __('Poland', 'yatra'),
78 + 'PT' => __('Portugal', 'yatra'), 'QA' => __('Qatar', 'yatra'), 'RO' => __('Romania', 'yatra'), 'RU' => __('Russia', 'yatra'),
79 + 'RW' => __('Rwanda', 'yatra'), 'KN' => __('Saint Kitts and Nevis', 'yatra'), 'LC' => __('Saint Lucia', 'yatra'),
80 + 'VC' => __('Saint Vincent and the Grenadines', 'yatra'), 'WS' => __('Samoa', 'yatra'), 'SM' => __('San Marino', 'yatra'),
81 + 'ST' => __('Sao Tome and Principe', 'yatra'), 'SA' => __('Saudi Arabia', 'yatra'), 'SN' => __('Senegal', 'yatra'),
82 + 'RS' => __('Serbia', 'yatra'), 'SC' => __('Seychelles', 'yatra'), 'SL' => __('Sierra Leone', 'yatra'), 'SG' => __('Singapore', 'yatra'),
83 + 'SK' => __('Slovakia', 'yatra'), 'SI' => __('Slovenia', 'yatra'), 'SB' => __('Solomon Islands', 'yatra'), 'SO' => __('Somalia', 'yatra'),
84 + 'ZA' => __('South Africa', 'yatra'), 'SS' => __('South Sudan', 'yatra'), 'ES' => __('Spain', 'yatra'), 'LK' => __('Sri Lanka', 'yatra'),
85 + 'SD' => __('Sudan', 'yatra'), 'SR' => __('Suriname', 'yatra'), 'SE' => __('Sweden', 'yatra'), 'CH' => __('Switzerland', 'yatra'),
86 + 'SY' => __('Syria', 'yatra'), 'TW' => __('Taiwan', 'yatra'), 'TJ' => __('Tajikistan', 'yatra'), 'TZ' => __('Tanzania', 'yatra'),
87 + 'TH' => __('Thailand', 'yatra'), 'TL' => __('Timor-Leste', 'yatra'), 'TG' => __('Togo', 'yatra'), 'TO' => __('Tonga', 'yatra'),
88 + 'TT' => __('Trinidad and Tobago', 'yatra'), 'TN' => __('Tunisia', 'yatra'), 'TR' => __('Turkey', 'yatra'), 'TM' => __('Turkmenistan', 'yatra'),
89 + 'TV' => __('Tuvalu', 'yatra'), 'UG' => __('Uganda', 'yatra'), 'UA' => __('Ukraine', 'yatra'), 'AE' => __('United Arab Emirates', 'yatra'),
90 + 'GB' => __('United Kingdom', 'yatra'), 'US' => __('United States', 'yatra'), 'UY' => __('Uruguay', 'yatra'), 'UZ' => __('Uzbekistan', 'yatra'),
91 + 'VU' => __('Vanuatu', 'yatra'), 'VA' => __('Vatican City', 'yatra'), 'VE' => __('Venezuela', 'yatra'), 'VN' => __('Vietnam', 'yatra'),
92 + 'YE' => __('Yemen', 'yatra'), 'ZM' => __('Zambia', 'yatra'), 'ZW' => __('Zimbabwe', 'yatra'),
93 + // ISO-3166-1 territories and special regions added so customers
94 + // from common tourism markets (Hong Kong, Puerto Rico, Greenland,
95 + // Gibraltar, Faroe Islands, etc.) can select their location.
96 + 'HK' => __('Hong Kong', 'yatra'), 'MO' => __('Macao', 'yatra'), 'PR' => __('Puerto Rico', 'yatra'),
97 + 'GI' => __('Gibraltar', 'yatra'), 'GL' => __('Greenland', 'yatra'), 'FO' => __('Faroe Islands', 'yatra'),
98 + 'GG' => __('Guernsey', 'yatra'), 'IM' => __('Isle of Man', 'yatra'), 'JE' => __('Jersey', 'yatra'),
99 + 'AX' => __('Aland Islands', 'yatra'), 'SJ' => __('Svalbard and Jan Mayen', 'yatra'),
100 + 'BM' => __('Bermuda', 'yatra'), 'KY' => __('Cayman Islands', 'yatra'), 'AI' => __('Anguilla', 'yatra'),
101 + 'AW' => __('Aruba', 'yatra'), 'CW' => __('Curacao', 'yatra'), 'SX' => __('Sint Maarten', 'yatra'),
102 + 'BQ' => __('Bonaire, Sint Eustatius and Saba', 'yatra'),
103 + 'MS' => __('Montserrat', 'yatra'), 'VG' => __('British Virgin Islands', 'yatra'),
104 + 'TC' => __('Turks and Caicos Islands', 'yatra'),
105 + 'BL' => __('Saint Barthelemy', 'yatra'), 'MF' => __('Saint Martin (French)', 'yatra'),
106 + 'PM' => __('Saint Pierre and Miquelon', 'yatra'),
107 + 'SH' => __('Saint Helena, Ascension and Tristan da Cunha', 'yatra'),
108 + 'FK' => __('Falkland Islands', 'yatra'),
109 + 'GS' => __('South Georgia and the South Sandwich Islands', 'yatra'),
110 + 'PF' => __('French Polynesia', 'yatra'), 'NC' => __('New Caledonia', 'yatra'),
111 + 'WF' => __('Wallis and Futuna', 'yatra'), 'CK' => __('Cook Islands', 'yatra'),
112 + 'NU' => __('Niue', 'yatra'), 'TK' => __('Tokelau', 'yatra'),
113 + 'GP' => __('Guadeloupe', 'yatra'), 'MQ' => __('Martinique', 'yatra'),
114 + 'RE' => __('Reunion', 'yatra'), 'YT' => __('Mayotte', 'yatra'),
115 + 'AS' => __('American Samoa', 'yatra'), 'GU' => __('Guam', 'yatra'),
116 + 'MP' => __('Northern Mariana Islands', 'yatra'), 'VI' => __('U.S. Virgin Islands', 'yatra'),
117 + 'UM' => __('U.S. Minor Outlying Islands', 'yatra'),
118 + 'TF' => __('French Southern Territories', 'yatra'),
119 + 'IO' => __('British Indian Ocean Territory', 'yatra'), 'BV' => __('Bouvet Island', 'yatra'),
120 + 'HM' => __('Heard Island and McDonald Islands', 'yatra'),
121 + 'AQ' => __('Antarctica', 'yatra'), 'PN' => __('Pitcairn', 'yatra'),
122 + 'EH' => __('Western Sahara', 'yatra'), 'XK' => __('Kosovo', 'yatra'),
123 + ];
124 + }
125 +
77 126 /**
78 127 * Format price with currency
79 128 *
80 129 * @param float $amount Amount to format
@@ -83,9 +132,19 @@
83 132 * @return string Formatted price
84 133 */
85 134 public static function formatPrice(float $amount, string $currency = 'USD', bool $showCode = false): string
86 135 {
87 - return CurrencyHelper::format($amount, $currency, $showCode);
136 + if (function_exists('yatra_format_price')) {
137 + $main = yatra_format_price($amount, $currency, false);
138 + } else {
139 + $main = CurrencyHelper::format($amount, $currency, false);
140 + }
141 +
142 + if ($showCode) {
143 + return $main . ' ' . strtoupper($currency);
144 + }
145 +
146 + return $main;
88 147 }
89 148
90 149 /**
91 150 * Get currency symbol
@@ -110,9 +169,9 @@
110 169 if (empty($date)) {
111 170 return '';
112 171 }
113 172
114 - $format = $format ?: get_option('date_format', 'F j, Y');
173 + $format = $format ?: SettingsService::getString('date_format', (string) get_option('date_format', 'F j, Y'));
115 174
116 175 $timestamp = strtotime($date);
117 176 if ($timestamp === false) {
118 177 return $date;
@@ -117,9 +176,9 @@
117 176 if ($timestamp === false) {
118 177 return $date;
119 178 }
120 179
121 - return date_i18n($format, $timestamp);
180 + return self::formatTimestampWithTz($timestamp, $format);
122 181 }
123 182
124 183 /**
125 184 * Format datetime for display
@@ -133,10 +192,10 @@
133 192 if (empty($datetime)) {
134 193 return '';
135 194 }
136 195
137 - $dateFormat = get_option('date_format', 'F j, Y');
138 - $timeFormat = get_option('time_format', 'g:i a');
196 + $dateFormat = SettingsService::getString('date_format', (string) get_option('date_format', 'F j, Y'));
197 + $timeFormat = SettingsService::getString('time_format', (string) get_option('time_format', 'g:i a'));
139 198 $format = $format ?: $dateFormat . ' ' . $timeFormat;
140 199
141 200 $timestamp = strtotime($datetime);
142 201 if ($timestamp === false) {
@@ -142,13 +201,13 @@
142 201 if ($timestamp === false) {
143 202 return $datetime;
144 203 }
145 204
146 - return date_i18n($format, $timestamp);
205 + return self::formatTimestampWithTz($timestamp, $format);
147 206 }
148 207
149 208 /**
150 - * Format time for display (24h to 12h) with uppercase AM/PM
209 + * Format time for display
151 210 *
152 211 * @param string $time Time string (e.g., "14:30")
153 212 * @return string Formatted time (e.g., "2:30 PM")
154 213 */
@@ -153,23 +212,13 @@
153 212 * @return string Formatted time (e.g., "2:30 PM")
154 213 */
155 214 public static function formatTime(string $time): string
156 215 {
157 - if (empty($time)) {
158 - return '';
159 - }
160 -
161 - $timestamp = strtotime($time);
162 - if ($timestamp === false) {
163 - return $time;
164 - }
165 -
166 - // Always use uppercase AM/PM format (g:i A)
167 - return date_i18n('g:i A', $timestamp);
216 + return self::formatTimeForDisplay($time);
168 217 }
169 218
170 219 /**
171 - * Format time for display with uppercase AM/PM
220 + * Format time for display using plugin settings (fallback to WordPress settings)
172 221 *
173 222 * @param string $time Time string (e.g., "14:30")
174 223 * @return string Formatted time (e.g., "2:30 PM")
175 224 */
@@ -183,13 +232,48 @@
183 232 if ($timestamp === false) {
184 233 return $time;
185 234 }
186 235
187 - // Always use uppercase AM/PM format (g:i A)
188 - return date_i18n('g:i A', $timestamp);
236 + $format = SettingsService::getString('time_format', (string) get_option('time_format', 'g:i a'));
237 + return self::formatTimestampWithTz($timestamp, $format);
189 238 }
190 239
191 240 /**
241 + * Format a timestamp using Yatra timezone when set.
242 + *
243 + * @param int $timestamp Unix timestamp
244 + * @param string $format PHP date format
245 + * @return string
246 + */
247 + private static function formatTimestampWithTz(int $timestamp, string $format): string
248 + {
249 + $tz = trim((string) SettingsService::getString('timezone', ''));
250 + if ($tz === '') {
251 + // WP site timezone fallback
252 + return function_exists('wp_date')
253 + ? wp_date($format, $timestamp)
254 + : date_i18n($format, $timestamp);
255 + }
256 +
257 + try {
258 + $dtz = new \DateTimeZone($tz);
259 + } catch (\Exception $e) {
260 + return function_exists('wp_date')
261 + ? wp_date($format, $timestamp)
262 + : date_i18n($format, $timestamp);
263 + }
264 +
265 + if (function_exists('wp_date')) {
266 + return wp_date($format, $timestamp, $dtz);
267 + }
268 +
269 + // Older WP fallback: shift timestamp into requested timezone via DateTime.
270 + $d = new \DateTime('@' . $timestamp);
271 + $d->setTimezone($dtz);
272 + return $d->format($format);
273 + }
274 +
275 + /**
192 276 * Get relative time (e.g., "2 hours ago")
193 277 *
194 278 * @param string $datetime Datetime string
195 279 * @return string Relative time
@@ -216,22 +300,412 @@
216 300 */
217 301 public static function getCountryName(string $code): string
218 302 {
219 303 $code = strtoupper($code);
220 - return self::COUNTRY_NAMES[$code] ?? $code;
304 + return self::countryNames()[$code] ?? $code;
221 305 }
222 306
223 307 /**
224 - * Get all countries as options array
225 - *
226 - * @return array [code => name]
308 + * Build a customer's postal address as display lines, for invoices and any
309 + * other document that has to show where the customer actually is.
310 + *
311 + * Two sources, in order of authority:
312 + * 1. The booking's own contact data — what the customer entered at the time
313 + * of purchase, which is what an invoice should reflect.
314 + * 2. The linked customer record, which carries the structured city / state /
315 + * postal code / country that the booking form does not always collect.
316 + *
317 + * @param object|array|null $booking Booking (or payment joined to one) carrying
318 + * contact_data / contact_country / customer_id.
319 + * @return string[] Non-empty address lines, ready to render one per line.
227 320 */
321 + public static function customerAddressLines($booking): array
322 + {
323 + $get = static function ($source, string $key) {
324 + if (is_array($source)) {
325 + return $source[$key] ?? null;
326 + }
327 + if (is_object($source)) {
328 + return $source->$key ?? null;
329 + }
330 + return null;
331 + };
332 +
333 + $parts = ['address' => '', 'city' => '', 'state' => '', 'postal_code' => '', 'country' => ''];
334 +
335 + // 1. What the customer typed when booking.
336 + $contactData = $get($booking, 'contact_data');
337 + if (is_string($contactData) && $contactData !== '') {
338 + $contactData = json_decode($contactData, true);
339 + }
340 + if (is_array($contactData)) {
341 + foreach (array_keys($parts) as $key) {
342 + if (!empty($contactData[$key]) && is_scalar($contactData[$key])) {
343 + $parts[$key] = trim((string) $contactData[$key]);
344 + }
345 + }
346 + // The booking form stores the postcode under either name.
347 + if ($parts['postal_code'] === '' && !empty($contactData['zip'])) {
348 + $parts['postal_code'] = trim((string) $contactData['zip']);
349 + }
350 + }
351 +
352 + if ($parts['country'] === '') {
353 + $parts['country'] = trim((string) ($get($booking, 'contact_country') ?? ''));
354 + }
355 +
356 + // 2. Fill the gaps from the customer record.
357 + $customerId = (int) ($get($booking, 'customer_id') ?? 0);
358 + if ($customerId > 0 && in_array('', $parts, true)) {
359 + $customer = (new \Yatra\Repositories\CustomerRepository())->find($customerId);
360 +
361 + if ($customer) {
362 + foreach (array_keys($parts) as $key) {
363 + if ($parts[$key] === '' && !empty($customer->$key)) {
364 + $parts[$key] = trim((string) $customer->$key);
365 + }
366 + }
367 + }
368 + }
369 +
370 + // "City, State 12345" reads as one line; street and country get their own.
371 + $locality = trim(implode(', ', array_filter([$parts['city'], $parts['state']])));
372 + if ($parts['postal_code'] !== '') {
373 + $locality = trim($locality . ' ' . $parts['postal_code']);
374 + }
375 +
376 + $country = $parts['country'] !== '' ? self::getCountryName($parts['country']) : '';
377 +
378 + return array_values(array_filter([$parts['address'], $locality, $country], static function ($line) {
379 + return $line !== '';
380 + }));
381 + }
382 +
383 + /**
384 + * The operator's own business address, one entry per line.
385 + *
386 + * Settings hold the address in separate fields (street, city, state, postcode,
387 + * country) but documents only ever printed `company_address` — so everything
388 + * after the street silently vanished and the address looked truncated at its
389 + * final line. Composed the same way as {@see customerAddressLines()} so both
390 + * sides of a document read identically.
391 + *
392 + * @return list<string>
393 + */
394 + public static function companyAddressLines(): array
395 + {
396 + $get = static function (string $key): string {
397 + return trim((string) \Yatra\Services\SettingsService::get($key, ''));
398 + };
399 +
400 + $street = $get('company_address');
401 + $city = $get('company_city');
402 + $state = $get('company_state');
403 + $zip = $get('company_zip');
404 + $countryRaw = $get('company_country');
405 +
406 + // "City, State 12345" reads as one line; street and country get their own.
407 + $locality = trim(implode(', ', array_filter([$city, $state])));
408 + if ($zip !== '') {
409 + $locality = trim($locality . ' ' . $zip);
410 + }
411 +
412 + $country = $countryRaw !== '' ? self::getCountryName($countryRaw) : '';
413 +
414 + // The street field may itself carry line breaks (an operator pasting a full
415 + // address), so keep those as separate lines rather than one run-on entry.
416 + $lines = [];
417 + foreach (preg_split('/\R/', $street) ?: [] as $streetLine) {
418 + $streetLine = trim($streetLine);
419 + if ($streetLine !== '') {
420 + $lines[] = $streetLine;
421 + }
422 + }
423 +
424 + foreach ([$locality, $country] as $line) {
425 + if ($line !== '') {
426 + $lines[] = $line;
427 + }
428 + }
429 +
430 + return array_values($lines);
431 + }
432 +
433 + /**
434 + * Canonical country list — single source of truth used by every
435 + * country / nationality dropdown in both Free and Pro plugins.
436 + *
437 + * Returns the full ISO-3166-1 alpha-2 set (sovereign states +
438 + * dependent territories + commonly-accepted regions). Sorted
439 + * alphabetically by name so the rendered dropdown is browseable
440 + * without operators having to scan a code-ordered list.
441 + *
442 + * Filterable via `yatra_countries_list` for operators that want
443 + * to:
444 + * - Prepend "popular" entries (US, GB, IN, etc.) above an
445 + * `---` separator for faster picking
446 + * - Remove territories that don't apply to their market
447 + * - Rename a region (e.g. business-language preferences)
448 + *
449 + * @return array<string, string> [ISO-3166 code => display name]
450 + */
228 451 public static function getCountries(): array
229 452 {
230 - return self::COUNTRY_NAMES;
453 + $countries = self::countryNames();
454 +
455 + // Stable alphabetical sort by display name. Operators expect
456 + // "Argentina" before "Australia" before "Austria" — code-order
457 + // (AR/AU/AT) is computer-friendly but not human-friendly.
458 + asort($countries, SORT_STRING | SORT_FLAG_CASE);
459 +
460 + if (\function_exists('apply_filters')) {
461 + $filtered = apply_filters('yatra_countries_list', $countries);
462 + if (\is_array($filtered) && $filtered !== []) {
463 + return $filtered;
464 + }
465 + }
466 + return $countries;
231 467 }
232 468
233 469 /**
470 + * International dialing codes, keyed by ISO-3166-1 alpha-2 (digits only,
471 + * no leading "+"). Aligned to the {@see countryNames()} set. Multiple
472 + * countries legitimately share a code (all NANP countries use "1", RU/KZ
473 + * use "7", GB/GG/IM/JE use "44") — {@see dialingCodePriority()} resolves
474 + * which one an incoming "+code" auto-selects.
475 + *
476 + * @return array<string, string> [ISO code => dialing code]
477 + */
478 + private static function dialingCodes(): array
479 + {
480 + return [
481 + 'AF' => '93', 'AL' => '355', 'DZ' => '213', 'AD' => '376', 'AO' => '244', 'AI' => '1', 'AQ' => '672',
482 + 'AG' => '1', 'AR' => '54', 'AM' => '374', 'AW' => '297', 'AU' => '61', 'AT' => '43', 'AZ' => '994',
483 + 'BS' => '1', 'BH' => '973', 'BD' => '880', 'BB' => '1', 'BY' => '375', 'BE' => '32', 'BZ' => '501',
484 + 'BJ' => '229', 'BM' => '1', 'BT' => '975', 'BO' => '591', 'BQ' => '599', 'BA' => '387', 'BW' => '267',
485 + 'BR' => '55', 'IO' => '246', 'BN' => '673', 'BG' => '359', 'BF' => '226', 'BI' => '257', 'KH' => '855',
486 + 'CM' => '237', 'CA' => '1', 'CV' => '238', 'KY' => '1', 'CF' => '236', 'TD' => '235', 'CL' => '56',
487 + 'CN' => '86', 'CO' => '57', 'KM' => '269', 'CG' => '242', 'CD' => '243', 'CK' => '682', 'CR' => '506',
488 + 'CI' => '225', 'HR' => '385', 'CU' => '53', 'CW' => '599', 'CY' => '357', 'CZ' => '420', 'DK' => '45',
489 + 'DJ' => '253', 'DM' => '1', 'DO' => '1', 'EC' => '593', 'EG' => '20', 'SV' => '503', 'GQ' => '240',
490 + 'ER' => '291', 'EE' => '372', 'SZ' => '268', 'ET' => '251', 'FK' => '500', 'FO' => '298', 'FJ' => '679',
491 + 'FI' => '358', 'FR' => '33', 'GF' => '594', 'PF' => '689', 'GA' => '241', 'GM' => '220', 'GE' => '995',
492 + 'DE' => '49', 'GH' => '233', 'GI' => '350', 'GR' => '30', 'GL' => '299', 'GD' => '1', 'GP' => '590',
493 + 'GU' => '1', 'GT' => '502', 'GG' => '44', 'GN' => '224', 'GW' => '245', 'GY' => '592', 'HT' => '509',
494 + 'HN' => '504', 'HK' => '852', 'HU' => '36', 'IS' => '354', 'IN' => '91', 'ID' => '62', 'IR' => '98',
495 + 'IQ' => '964', 'IE' => '353', 'IM' => '44', 'IL' => '972', 'IT' => '39', 'JM' => '1', 'JP' => '81',
496 + 'JE' => '44', 'JO' => '962', 'KZ' => '7', 'KE' => '254', 'KI' => '686', 'KP' => '850', 'KR' => '82',
497 + 'KW' => '965', 'KG' => '996', 'LA' => '856', 'LV' => '371', 'LB' => '961', 'LS' => '266', 'LR' => '231',
498 + 'LY' => '218', 'LI' => '423', 'LT' => '370', 'LU' => '352', 'MO' => '853', 'MK' => '389', 'MG' => '261',
499 + 'MW' => '265', 'MY' => '60', 'MV' => '960', 'ML' => '223', 'MT' => '356', 'MH' => '692', 'MQ' => '596',
500 + 'MR' => '222', 'MU' => '230', 'YT' => '262', 'MX' => '52', 'FM' => '691', 'MD' => '373', 'MC' => '377',
501 + 'MN' => '976', 'ME' => '382', 'MS' => '1', 'MA' => '212', 'MZ' => '258', 'MM' => '95', 'NA' => '264',
502 + 'NR' => '674', 'NP' => '977', 'NL' => '31', 'NC' => '687', 'NZ' => '64', 'NI' => '505', 'NE' => '227',
503 + 'NG' => '234', 'NU' => '683', 'NF' => '672', 'MP' => '1', 'NO' => '47', 'OM' => '968', 'PK' => '92',
504 + 'PW' => '680', 'PS' => '970', 'PA' => '507', 'PG' => '675', 'PY' => '595', 'PE' => '51', 'PH' => '63',
505 + 'PN' => '64', 'PL' => '48', 'PT' => '351', 'PR' => '1', 'QA' => '974', 'RE' => '262', 'RO' => '40',
506 + 'RU' => '7', 'RW' => '250', 'BL' => '590', 'SH' => '290', 'KN' => '1', 'LC' => '1', 'MF' => '590',
507 + 'PM' => '508', 'VC' => '1', 'WS' => '685', 'SM' => '378', 'ST' => '239', 'SA' => '966', 'SN' => '221',
508 + 'RS' => '381', 'SC' => '248', 'SL' => '232', 'SG' => '65', 'SX' => '1', 'SK' => '421', 'SI' => '386',
509 + 'SB' => '677', 'SO' => '252', 'ZA' => '27', 'GS' => '500', 'SS' => '211', 'ES' => '34', 'LK' => '94',
510 + 'SD' => '249', 'SR' => '597', 'SJ' => '47', 'SE' => '46', 'CH' => '41', 'SY' => '963', 'TW' => '886',
511 + 'TJ' => '992', 'TZ' => '255', 'TH' => '66', 'TL' => '670', 'TG' => '228', 'TK' => '690', 'TO' => '676',
512 + 'TT' => '1', 'TN' => '216', 'TR' => '90', 'TM' => '993', 'TC' => '1', 'TV' => '688', 'UG' => '256',
513 + 'UA' => '380', 'AE' => '971', 'GB' => '44', 'US' => '1', 'UY' => '598', 'UZ' => '998', 'VU' => '678',
514 + 'VA' => '39', 'VE' => '58', 'VN' => '84', 'VG' => '1', 'VI' => '1', 'WF' => '681', 'EH' => '212',
515 + 'YE' => '967', 'ZM' => '260', 'ZW' => '263', 'XK' => '383', 'AS' => '1', 'AX' => '358',
516 + 'UM' => '1', 'CX' => '61', 'CC' => '61', 'TA' => '290',
517 + ];
518 + }
519 +
520 + /**
521 + * Preferred country for a dialing code that several countries share. When
522 + * an operator types a raw "+<code><number>" we must pick one country to
523 + * auto-select; this is the conventional primary (e.g. "1" => US, not one of
524 + * the ~20 other NANP countries). Codes not listed here fall back to the
525 + * first ISO that declares them.
526 + *
527 + * @return array<string, string> [dialing code => primary ISO]
528 + */
529 + private static function dialingCodePriority(): array
530 + {
531 + return [
532 + '1' => 'US', '7' => 'RU', '39' => 'IT', '44' => 'GB', '47' => 'NO', '61' => 'AU',
533 + '212' => 'MA', '262' => 'RE', '290' => 'SH', '358' => 'FI', '500' => 'FK',
534 + '590' => 'GP', '599' => 'CW', '64' => 'NZ', '672' => 'AQ',
535 + ];
536 + }
537 +
538 + /**
539 + * Preferred ISO per shared dialing code (dial => ISO), for the front-end
540 + * detector to resolve which country a "+code" auto-selects. Mirrors the
541 + * server-side {@see dialingCodePriority()}.
542 + *
543 + * @return array<string, string>
544 + */
545 + public static function getPhonePriority(): array
546 + {
547 + return self::dialingCodePriority();
548 + }
549 +
550 + /**
551 + * Dialing code (digits only, no "+") for one ISO code, or '' if unknown.
552 + */
553 + public static function getDialingCode(string $iso): string
554 + {
555 + $codes = self::dialingCodes();
556 + return $codes[strtoupper($iso)] ?? '';
557 + }
558 +
559 + /**
560 + * Country list for the phone-number field: ISO code, display name and
561 + * dialing code, sorted by display name. Filterable via
562 + * `yatra_phone_countries` (e.g. to pin popular countries to the top).
563 + *
564 + * @return array<int, array{iso:string, name:string, dial:string}>
565 + */
566 + public static function getPhoneCountries(): array
567 + {
568 + $names = self::getCountries(); // [iso => name], already sorted/filtered
569 + $dial = self::dialingCodes();
570 + $out = [];
571 + foreach ($names as $iso => $name) {
572 + $iso = strtoupper((string) $iso);
573 + if (!isset($dial[$iso]) || $dial[$iso] === '') {
574 + continue;
575 + }
576 + $out[] = ['iso' => $iso, 'name' => (string) $name, 'dial' => $dial[$iso]];
577 + }
578 + if (\function_exists('apply_filters')) {
579 + $filtered = apply_filters('yatra_phone_countries', $out);
580 + if (\is_array($filtered) && $filtered !== []) {
581 + return $filtered;
582 + }
583 + }
584 + return $out;
585 + }
586 +
587 + /**
588 + * Detect the country/dialing-code from a raw phone value. Only a value that
589 + * begins with "+" is treated as international; anything else returns null so
590 + * an existing bare national number is left untouched (backward compatible).
591 + *
592 + * Uses longest-prefix matching so "+9779806015400" → NP (977), not a shorter
593 + * false match, then applies {@see dialingCodePriority()} for shared codes.
594 + *
595 + * @return array{iso:string, dial:string, national:string}|null
596 + */
597 + public static function detectPhoneCountry(string $raw): ?array
598 + {
599 + $raw = trim($raw);
600 + if ($raw === '' || $raw[0] !== '+') {
601 + return null;
602 + }
603 + $digits = preg_replace('/\D+/', '', $raw);
604 + if ($digits === '' || $digits === null) {
605 + return null;
606 + }
607 + $codes = self::dialingCodes();
608 + $priority = self::dialingCodePriority();
609 + $maxLen = min(4, strlen($digits));
610 + for ($len = $maxLen; $len >= 1; $len--) {
611 + $prefix = substr($digits, 0, $len);
612 + $matches = array_keys($codes, $prefix, true);
613 + if ($matches === []) {
614 + continue;
615 + }
616 + $iso = $priority[$prefix] ?? (string) $matches[0];
617 + return [
618 + 'iso' => $iso,
619 + 'dial' => $prefix,
620 + 'national' => substr($digits, $len),
621 + ];
622 + }
623 + return null;
624 + }
625 +
626 + /**
627 + * Default country pre-selected in the phone field. Priority: the
628 + * `default_phone_country` setting → the site locale suffix (en_US → US,
629 + * ne_NP → NP) → "US". Filterable via `yatra_default_phone_country`.
630 + */
631 + public static function getDefaultPhoneCountry(): string
632 + {
633 + $codes = self::dialingCodes();
634 + $default = '';
635 +
636 + if (\class_exists(SettingsService::class)) {
637 + $setting = strtoupper((string) SettingsService::getString('default_phone_country', ''));
638 + if ($setting !== '' && isset($codes[$setting])) {
639 + $default = $setting;
640 + }
641 + }
642 + if ($default === '' && \function_exists('get_locale')) {
643 + if (preg_match('/_([A-Z]{2})$/', (string) get_locale(), $m) && isset($codes[$m[1]])) {
644 + $default = $m[1];
645 + }
646 + }
647 + if ($default === '') {
648 + $default = 'US';
649 + }
650 + if (\function_exists('apply_filters')) {
651 + $default = (string) apply_filters('yatra_default_phone_country', $default);
652 + }
653 + return isset($codes[$default]) ? $default : 'US';
654 + }
655 +
656 + /**
657 + * Combine a national number with a country (ISO) into the stored value.
658 + *
659 + * - Empty number → '' (nothing to store).
660 + * - Already-international ("+...") → returned unchanged (a pasted full number
661 + * or the no-JS fallback where the customer typed the "+code" themselves).
662 + * - National number + known ISO → "+<dial><digits>" (spaces/dashes stripped).
663 + * - No/unknown ISO → the number left as typed (legacy behavior, backward
664 + * compatible — nothing is invented).
665 + */
666 + public static function combineInternationalPhone(string $number, string $iso): string
667 + {
668 + $number = trim($number);
669 + if ($number === '') {
670 + return '';
671 + }
672 + if ($number[0] === '+') {
673 + return $number;
674 + }
675 + $iso = strtoupper(trim($iso));
676 + $dial = $iso !== '' ? self::getDialingCode($iso) : '';
677 + if ($dial === '') {
678 + return $number;
679 + }
680 + $digits = preg_replace('/\D+/', '', $number);
681 + if ($digits === '' || $digits === null) {
682 + return $number;
683 + }
684 + return '+' . $dial . $digits;
685 + }
686 +
687 + /**
688 + * Format a stored phone value for human display. A value saved with a
689 + * country code ("+9779806015400") is shown as "+977 9806015400"; a legacy
690 + * bare value is returned unchanged (never corrupted). Optionally accepts a
691 + * known ISO to disambiguate shared codes.
692 + */
693 + public static function formatPhoneForDisplay(string $stored, string $iso = ''): string
694 + {
695 + $stored = trim($stored);
696 + if ($stored === '') {
697 + return '';
698 + }
699 + $detected = self::detectPhoneCountry($stored);
700 + if ($detected === null) {
701 + return $stored; // legacy / national-only value — leave as-is
702 + }
703 + $national = $detected['national'] !== '' ? ' ' . $detected['national'] : '';
704 + return '+' . $detected['dial'] . $national;
705 + }
706 +
707 + /**
234 708 * Format phone number for display
235 709 *
236 710 * @param string $phone Phone number
237 711 * @return string Formatted phone
@@ -326,10 +800,20 @@
326 800 */
327 801 public static function formatRatingStars(float $rating, bool $showNumber = false): string
328 802 {
329 803 $rating = max(0, min(5, $rating));
330 - $fullStars = (int) floor($rating);
331 - $hasHalfStar = ($rating - $fullStars) >= 0.5;
804 +
805 + // Rounds to the nearest half star (see yatra_rating_star_parts), so this
806 + // renderer agrees with the confirmation page and reviews block.
807 + if (function_exists('yatra_rating_star_parts')) {
808 + $parts = yatra_rating_star_parts($rating);
809 + $fullStars = $parts['full'];
810 + $hasHalfStar = $parts['half'];
811 + } else {
812 + $halves = (int) round($rating * 2);
813 + $fullStars = intdiv($halves, 2);
814 + $hasHalfStar = ($halves % 2) === 1;
815 + }
332 816
333 817 $html = '<span class="yatra-rating-stars">';
334 818
335 819 for ($i = 1; $i <= 5; $i++) {