| @@ -7,13 +7,9 @@ | ||
| 7 | 7 | |
| 8 | 8 | import { __ } from "./i18n"; |
| 9 | 9 | import { getCurrency, getCurrencySymbol } from "../data/currencies"; |
| 10 | 10 | |
| 11 | -export type CurrencyPlacement = | |
| 12 | - | "left" | |
| 13 | - | "right" | |
| 14 | - | "left_space" | |
| 15 | - | "right_space"; | |
| 11 | +export type CurrencyPlacement = "left" | "right" | "left_space" | "right_space"; | |
| 16 | 12 | |
| 17 | 13 | /** |
| 18 | 14 | * Normalize option / legacy values to one of the four placement modes. |
| 19 | 15 | */ |
| @@ -26,9 +22,14 @@ | ||
| 26 | 22 | } |
| 27 | 23 | if (p === "after") { |
| 28 | 24 | return "right_space"; |
| 29 | 25 | } |
| 30 | - if (p === "right" || p === "right_space" || p === "left" || p === "left_space") { | |
| 26 | + if ( | |
| 27 | + p === "right" || | |
| 28 | + p === "right_space" || | |
| 29 | + p === "left" || | |
| 30 | + p === "left_space" | |
| 31 | + ) { | |
| 31 | 32 | return p as CurrencyPlacement; |
| 32 | 33 | } |
| 33 | 34 | return "left_space"; |
| 34 | 35 | } |
| @@ -93,31 +94,62 @@ | ||
| 93 | 94 | "before" |
| 94 | 95 | ); |
| 95 | 96 | } |
| 96 | 97 | |
| 97 | -function readAdminNumberFormatConfig(): { | |
| 98 | +function readYatraNumberFormatConfig(): { | |
| 98 | 99 | decimalPlaces: number; |
| 99 | 100 | thousandSeparator: string; |
| 100 | 101 | decimalSeparator: string; |
| 101 | 102 | } { |
| 103 | + const fallback = { | |
| 104 | + decimalPlaces: 2, | |
| 105 | + thousandSeparator: ",", | |
| 106 | + decimalSeparator: ".", | |
| 107 | + }; | |
| 102 | 108 | if (typeof window === "undefined") { |
| 103 | - return { decimalPlaces: 2, thousandSeparator: ",", decimalSeparator: "." }; | |
| 109 | + return fallback; | |
| 104 | 110 | } |
| 105 | - const a = ((window as AdminWindow).yatraAdmin || {}) as Record< | |
| 106 | - string, | |
| 107 | - unknown | |
| 108 | - >; | |
| 109 | - const decimals = Number(a.decimalPlaces ?? a.currency_decimals ?? 2) || 2; | |
| 111 | + // Read from the SAME set of globals as the currency position, not just | |
| 112 | + // yatraAdmin. On public pages (customer account, checkout, single trip) | |
| 113 | + // window.yatraAdmin is absent, so reading separators only from it made the | |
| 114 | + // decimal/thousand separators silently fall back to defaults ("." / ",") | |
| 115 | + // while the position — read from these public globals — was correct. That is | |
| 116 | + // the "decimal separator ignored in payment amounts" bug on the storefront. | |
| 117 | + const w = window as AdminWindow; | |
| 118 | + const sources: Array<Record<string, unknown> | undefined> = [ | |
| 119 | + w.yatraAdmin as Record<string, unknown> | undefined, | |
| 120 | + w.yatraBookingData as Record<string, unknown> | undefined, | |
| 121 | + w.yatraTripData as Record<string, unknown> | undefined, | |
| 122 | + w.yatraAccountPage as Record<string, unknown> | undefined, | |
| 123 | + ]; | |
| 124 | + const pickString = (field: string): string | undefined => { | |
| 125 | + for (const o of sources) { | |
| 126 | + const v = o?.[field]; | |
| 127 | + if (typeof v === "string" && v !== "") { | |
| 128 | + return v; | |
| 129 | + } | |
| 130 | + } | |
| 131 | + return undefined; | |
| 132 | + }; | |
| 133 | + const pickNumber = (...fields: string[]): number | undefined => { | |
| 134 | + for (const o of sources) { | |
| 135 | + if (!o) { | |
| 136 | + continue; | |
| 137 | + } | |
| 138 | + for (const field of fields) { | |
| 139 | + const v = o[field]; | |
| 140 | + if (v !== undefined && v !== null && v !== "" && !Number.isNaN(Number(v))) { | |
| 141 | + return Number(v); | |
| 142 | + } | |
| 143 | + } | |
| 144 | + } | |
| 145 | + return undefined; | |
| 146 | + }; | |
| 147 | + const decimals = pickNumber("decimalPlaces", "currency_decimals") ?? 2; | |
| 110 | 148 | return { |
| 111 | 149 | decimalPlaces: Math.max(0, Math.min(4, decimals)), |
| 112 | - thousandSeparator: | |
| 113 | - typeof a.thousandSeparator === "string" && a.thousandSeparator !== "" | |
| 114 | - ? (a.thousandSeparator as string) | |
| 115 | - : ",", | |
| 116 | - decimalSeparator: | |
| 117 | - typeof a.decimalSeparator === "string" && a.decimalSeparator !== "" | |
| 118 | - ? (a.decimalSeparator as string) | |
| 119 | - : ".", | |
| 150 | + thousandSeparator: pickString("thousandSeparator") ?? ",", | |
| 151 | + decimalSeparator: pickString("decimalSeparator") ?? ".", | |
| 120 | 152 | }; |
| 121 | 153 | } |
| 122 | 154 | |
| 123 | 155 | /** |
| @@ -136,9 +168,9 @@ | ||
| 136 | 168 | return __("Contact for pricing", "yatra"); |
| 137 | 169 | } |
| 138 | 170 | |
| 139 | 171 | const { decimalPlaces, thousandSeparator, decimalSeparator } = |
| 140 | - readAdminNumberFormatConfig(); | |
| 172 | + readYatraNumberFormatConfig(); | |
| 141 | 173 | const currencyData = getCurrency(currencyCode); |
| 142 | 174 | const decimals = |
| 143 | 175 | currencyData?.decimalDigits !== undefined |
| 144 | 176 | ? Math.max(0, Math.min(4, currencyData.decimalDigits)) |
| @@ -143,9 +175,13 @@ | ||
| 143 | 175 | currencyData?.decimalDigits !== undefined |
| 144 | 176 | ? Math.max(0, Math.min(4, currencyData.decimalDigits)) |
| 145 | 177 | : decimalPlaces; |
| 146 | 178 | |
| 147 | - const formattedCore = new Intl.NumberFormat(undefined, { | |
| 179 | + // Format with a FIXED "en-US" base (grouping ",", decimal ".") so the | |
| 180 | + // replace-with-configured-separators step below is deterministic. Using the | |
| 181 | + // viewer's locale here (undefined) would already yield locale separators and | |
| 182 | + // the naive comma/dot replacement would swap them for e.g. German browsers. | |
| 183 | + const formattedCore = new Intl.NumberFormat("en-US", { | |
| 148 | 184 | minimumFractionDigits: decimals, |
| 149 | 185 | maximumFractionDigits: decimals, |
| 150 | 186 | }) |
| 151 | 187 | .format(num) |